diff --git a/README.md b/README.md index 000ea87d..41700442 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,8 @@ To learn more about MapStruct have a look at the [mapstruct](https://github.com/ * `*` used as a source in `@Mapping` annotation with quick fixes: Replace `*` with method parameter name. * Unknown reference inspection for `source` and `target` in `@Mapping` and `@ValueMapping` annotation. * Unknown reference inspection for `qualifiedByName` in `@Mapping` annotation + * Subclass `source` mapped more than once by `@SubclassMapping` annotations with quick fixes: Remove annotation and change source property. + * Value `source` mapped more than once by `@ValueMapping` annotations with quick fixes: Remove annotation and change source property. ## Requirements diff --git a/build.gradle b/build.gradle index 6c8f86ad..ea384e56 100644 --- a/build.gradle +++ b/build.gradle @@ -105,7 +105,7 @@ dependencies { testFramework( TestFrameworkType.Platform.INSTANCE ) testFramework( TestFrameworkType.Plugin.Java.INSTANCE ) } - implementation('org.mapstruct:mapstruct:1.5.3.Final') + implementation('org.mapstruct:mapstruct:1.6.3') testImplementation(platform('org.junit:junit-bom:5.11.0')) testImplementation('org.junit.platform:junit-platform-launcher') testImplementation('org.junit.jupiter:junit-jupiter-api') @@ -124,7 +124,7 @@ tasks.register('libs', Sync) { include('mapstruct-intellij-*.jar') include('MapStruct-Intellij-*.jar') } - rename('mapstruct-1.5.3.Final.jar', 'mapstruct.jar') + rename('mapstruct-1.6.3.jar', 'mapstruct.jar') } tasks.register('testLibs', Sync) { diff --git a/description.html b/description.html index 7c30808c..c49c48c9 100644 --- a/description.html +++ b/description.html @@ -45,6 +45,8 @@
  • * used as a source in @Mapping annotations with quick fixes: Replace * with method parameter name.
  • Unknown reference inspection for source and target in @Mapping and @ValueMapping annotation.
  • Unknown reference inspection for qualifiedByName in @Mapping annotation.
  • +
  • Subclass source mapped more than once by @SubclassMapping annotations with quick fixes: Remove annotation and change source property.
  • +
  • Value source mapped more than once by @ValueMapping annotations with quick fixes: Remove annotation and change source property.
  • diff --git a/src/main/java/org/mapstruct/intellij/inspection/MoreThanOnceMappedAnnotationInspectionBase.java b/src/main/java/org/mapstruct/intellij/inspection/MoreThanOnceMappedAnnotationInspectionBase.java new file mode 100644 index 00000000..9b5ee202 --- /dev/null +++ b/src/main/java/org/mapstruct/intellij/inspection/MoreThanOnceMappedAnnotationInspectionBase.java @@ -0,0 +1,246 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at https://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.intellij.inspection; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Stream; + +import com.intellij.codeInsight.intention.QuickFixFactory; +import com.intellij.codeInspection.LocalQuickFix; +import com.intellij.codeInspection.LocalQuickFixAndIntentionActionOnPsiElement; +import com.intellij.codeInspection.LocalQuickFixOnPsiElement; +import com.intellij.codeInspection.ProblemsHolder; +import com.intellij.codeInspection.util.IntentionFamilyName; +import com.intellij.codeInspection.util.IntentionName; +import com.intellij.openapi.editor.CaretState; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.editor.LogicalPosition; +import com.intellij.openapi.editor.ScrollType; +import com.intellij.openapi.fileEditor.FileEditor; +import com.intellij.openapi.fileEditor.FileEditorManager; +import com.intellij.openapi.fileEditor.TextEditor; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.util.text.Strings; +import com.intellij.psi.JavaElementVisitor; +import com.intellij.psi.PsiAnnotation; +import com.intellij.psi.PsiAnnotationMemberValue; +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiElementVisitor; +import com.intellij.psi.PsiFile; +import com.intellij.psi.PsiMethod; +import com.intellij.psi.PsiModifierListOwner; +import com.intellij.psi.PsiType; +import com.intellij.psi.impl.source.tree.java.PsiAnnotationImpl; +import org.jetbrains.annotations.NotNull; +import org.mapstruct.intellij.MapStructBundle; +import org.mapstruct.intellij.util.MapStructVersion; +import org.mapstruct.intellij.util.MapstructUtil; + +import static org.mapstruct.intellij.util.TargetUtils.getTargetType; + +/** + * @author hduelme + * @param type of the problemElement + */ +public abstract class MoreThanOnceMappedAnnotationInspectionBase extends InspectionBase { + + @Override + @NotNull PsiElementVisitor buildVisitorInternal(@NotNull ProblemsHolder holder, boolean isOnTheFly ) { + return new MyJavaElementVisitor( holder, + MapstructUtil.resolveMapStructProjectVersion( holder.getFile() ) ); + } + + private class MyJavaElementVisitor extends JavaElementVisitor { + private final ProblemsHolder holder; + private final MapStructVersion mapStructVersion; + + private MyJavaElementVisitor(ProblemsHolder holder, MapStructVersion mapStructVersion) { + this.holder = holder; + this.mapStructVersion = mapStructVersion; + } + + @Override + public void visitMethod(PsiMethod method) { + if ( !MapstructUtil.isMapper( method.getContainingClass() ) ) { + return; + } + PsiType targetType = getTargetType( method ); + if ( targetType == null ) { + return; + } + Map> problemMap = new HashMap<>(); + for ( PsiAnnotation psiAnnotation : method.getAnnotations() ) { + String qualifiedName = psiAnnotation.getQualifiedName(); + if ( getSingleMappingAnnotationFqn().equals( qualifiedName ) ) { + handleSingleMappingAnnotation( psiAnnotation, problemMap ); + } + else if ( getRepeatableMappingsAnnotationFqn().equals( qualifiedName ) ) { + extractAnnotationsFromRepeatableMappingsAnnotation( psiAnnotation ) + .forEach( a -> handleSingleMappingAnnotation( a, problemMap ) ); + } + else { + // Handle annotations containing at least one Mapping annotation + handleAnnotationWithMappingAnnotations( psiAnnotation, problemMap ); + } + } + QuickFixFactory quickFixFactory = QuickFixFactory.getInstance(); + for ( Map.Entry> problem : problemMap.entrySet() ) { + List problemElements = problem.getValue(); + if ( problemElements.size() > 1 ) { + for ( PsiElement problemElement : problemElements ) { + LocalQuickFix[] quickFixes = getLocalQuickFixes( problemElement, quickFixFactory ); + holder.registerProblem( problemElement, + getProblemDescription( problem.getKey() ), quickFixes ); + } + } + } + } + + private @NotNull LocalQuickFix[] getLocalQuickFixes(PsiElement problemElement, + QuickFixFactory quickFixFactory) { + List quickFixes = new ArrayList<>(2); + if ( problemElement instanceof PsiAnnotation ) { + quickFixes.add( getDeleteFix( problemElement, quickFixFactory ) ); + // Todo Goto quick Fix + } + else if ( problemElement instanceof PsiAnnotationMemberValue problemPsiAnnotationMemberValue ) { + Optional.ofNullable( problemElement.getParent() ).map( PsiElement::getParent ) + .map( PsiElement::getParent ).filter( PsiAnnotation.class::isInstance ) + .ifPresent( annotation -> quickFixes.add( + getDeleteFix( annotation, quickFixFactory ) ) ); + quickFixes.add( getChangeTargetQuickFix( problemPsiAnnotationMemberValue ) ); + } + return quickFixes.toArray( new LocalQuickFix[]{} ); + } + + private static @NotNull LocalQuickFixAndIntentionActionOnPsiElement getDeleteFix( + @NotNull PsiElement problemElement, @NotNull QuickFixFactory quickFixFactory) { + + String annotationName = PsiAnnotationImpl.getAnnotationShortName( problemElement.getText() ); + return quickFixFactory.createDeleteFix( problemElement, + MapStructBundle.message( "intention.remove.annotation", annotationName ) ); + } + + private void handleSingleMappingAnnotation(@NotNull PsiAnnotation psiAnnotation, + @NotNull Map> problemMap) { + PsiAnnotationMemberValue value = psiAnnotation.findDeclaredAttributeValue( getAttributeName() ); + if ( value == null ) { + return; + } + extractCompareKeyFromAnnotationMember( value ) + .ifPresent( key -> problemMap.computeIfAbsent( key, s -> new ArrayList<>() ).add( value ) ); + } + + private void handleAnnotationWithMappingAnnotations(PsiAnnotation psiAnnotation, + Map> problemMap) { + PsiClass annotationClass = psiAnnotation.resolveAnnotationType(); + if ( annotationClass == null ) { + return; + } + findAllDefinedMappings( annotationClass, mapStructVersion ) + .forEach( target -> { + PsiAnnotationMemberValue value = target.findDeclaredAttributeValue( getAttributeName() ); + if ( value == null ) { + return; + } + extractCompareKeyFromAnnotationMember( value ) + .ifPresent( key -> problemMap.computeIfAbsent( key, s -> new ArrayList<>() ) + .add( psiAnnotation ) ); + } ); + } + + } + + @NotNull + protected abstract String getSingleMappingAnnotationFqn(); + + @NotNull + protected abstract String getRepeatableMappingsAnnotationFqn(); + + @NotNull + protected abstract String getAttributeName(); + + @NotNull + protected abstract Stream findAllDefinedMappings(@NotNull PsiModifierListOwner owner, + @NotNull MapStructVersion mapStructVersion); + + protected abstract Optional extractCompareKeyFromAnnotationMember( + @NotNull PsiAnnotationMemberValue annotationMemberValue ); + + protected abstract LocalQuickFix getChangeTargetQuickFix( + @NotNull PsiAnnotationMemberValue problemPsiAnnotationMemberValue); + + protected abstract String getProblemDescription(@NotNull K problemKey); + + @NotNull + protected abstract Stream extractAnnotationsFromRepeatableMappingsAnnotation( + @NotNull PsiAnnotation mappings); + + protected static class ChangeTargetStringQuickFixBase extends LocalQuickFixOnPsiElement { + + private final String myText; + private final String myFamilyName; + + protected ChangeTargetStringQuickFixBase(@NotNull PsiAnnotationMemberValue element, + @NotNull String myText, @NotNull String myFamilyName) { + super( element ); + this.myText = myText; + this.myFamilyName = myFamilyName; + } + + @Override + public @IntentionName @NotNull String getText() { + return myText; + } + + @Override + public void invoke(@NotNull Project project, @NotNull PsiFile psiFile, @NotNull PsiElement psiElement, + @NotNull PsiElement psiElement1) { + FileEditor selectedEditor = FileEditorManager.getInstance( project ).getSelectedEditor(); + if ( selectedEditor instanceof TextEditor textEditor ) { + Editor editor = textEditor.getEditor(); + + TextRange textRange = psiElement.getTextRange(); + String textOfElement = String.valueOf( editor.getDocument() + .getCharsSequence() + .subSequence( textRange.getStartOffset(), textRange.getEndOffset() ) ); + int targetStart = Strings.indexOf( textOfElement, "\"" ) + 1; + int targetEnd = textOfElement.lastIndexOf( "\"" ); + + editor.getCaretModel().moveToOffset( textRange.getStartOffset() + targetStart ); + LogicalPosition startPosition = editor.getCaretModel().getLogicalPosition(); + editor.getCaretModel().moveToOffset( textRange.getStartOffset() + targetEnd ); + editor.getCaretModel().setCaretsAndSelections( + Collections.singletonList( new CaretState(startPosition, startPosition, + editor.getCaretModel().getLogicalPosition() ) ) ); + editor.getScrollingModel().scrollToCaret( ScrollType.MAKE_VISIBLE ); + } + } + + @Override + public @IntentionFamilyName @NotNull String getFamilyName() { + return myFamilyName; + } + + @Override + public boolean availableInBatchMode() { + return false; + } + + @Override + public boolean startInWriteAction() { + return false; + } + } + +} diff --git a/src/main/java/org/mapstruct/intellij/inspection/SubclassMappingSourceSubclassMappedMoreThanOnceInspection.java b/src/main/java/org/mapstruct/intellij/inspection/SubclassMappingSourceSubclassMappedMoreThanOnceInspection.java new file mode 100644 index 00000000..2c33476d --- /dev/null +++ b/src/main/java/org/mapstruct/intellij/inspection/SubclassMappingSourceSubclassMappedMoreThanOnceInspection.java @@ -0,0 +1,151 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at https://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.intellij.inspection; + +import java.util.Collections; +import java.util.Optional; +import java.util.stream.Stream; + +import com.intellij.codeInspection.LocalQuickFix; +import com.intellij.codeInspection.LocalQuickFixOnPsiElement; +import com.intellij.codeInspection.util.IntentionFamilyName; +import com.intellij.codeInspection.util.IntentionName; +import com.intellij.openapi.editor.CaretState; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.editor.LogicalPosition; +import com.intellij.openapi.editor.ScrollType; +import com.intellij.openapi.fileEditor.FileEditor; +import com.intellij.openapi.fileEditor.FileEditorManager; +import com.intellij.openapi.fileEditor.TextEditor; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.PsiAnnotation; +import com.intellij.psi.PsiAnnotationMemberValue; +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiClassObjectAccessExpression; +import com.intellij.psi.PsiClassType; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.intellij.psi.PsiModifierListOwner; +import com.intellij.psi.PsiType; +import org.jetbrains.annotations.NotNull; +import org.mapstruct.intellij.MapStructBundle; +import org.mapstruct.intellij.util.MapStructVersion; + +import static org.mapstruct.intellij.util.MapstructAnnotationUtils.extractSubclassMappingAnnotations; +import static org.mapstruct.intellij.util.MapstructAnnotationUtils.findAllDefinedSubclassMappingAnnotations; +import static org.mapstruct.intellij.util.MapstructUtil.SUBCLASS_MAPPINGS_ANNOTATION_FQN; +import static org.mapstruct.intellij.util.MapstructUtil.SUBCLASS_MAPPING_ANNOTATION_FQN; + +/** + * @author hduelme + */ +public class SubclassMappingSourceSubclassMappedMoreThanOnceInspection + extends MoreThanOnceMappedAnnotationInspectionBase { + + @NotNull + @Override + protected String getSingleMappingAnnotationFqn() { + return SUBCLASS_MAPPING_ANNOTATION_FQN; + } + + @NotNull + @Override + protected String getRepeatableMappingsAnnotationFqn() { + return SUBCLASS_MAPPINGS_ANNOTATION_FQN; + } + + @NotNull + @Override + protected String getAttributeName() { + return "source"; + } + + @NotNull + @Override + protected Stream findAllDefinedMappings(@NotNull PsiModifierListOwner owner, + @NotNull MapStructVersion mapStructVersion) { + return findAllDefinedSubclassMappingAnnotations( owner, true ); + } + + @Override + protected Optional extractCompareKeyFromAnnotationMember( + @NotNull PsiAnnotationMemberValue annotationMemberValue) { + if ( !( annotationMemberValue instanceof PsiClassObjectAccessExpression sourceClass ) ) { + return Optional.empty(); + } + PsiType sourceType = sourceClass.getOperand().getType(); + if ( sourceType instanceof PsiClassType classType ) { + return Optional.ofNullable( classType.resolve() ); + } + return Optional.empty(); + } + + @Override + protected LocalQuickFix getChangeTargetQuickFix(@NotNull PsiAnnotationMemberValue problemPsiAnnotationMemberValue) { + return new ChangeTargetQuickFix( problemPsiAnnotationMemberValue ); + } + + @Override + protected String getProblemDescription(@NotNull PsiClass problemKey) { + return MapStructBundle.message( "inspection.subclass.mapping.source.subclass.already.defined", + problemKey.getQualifiedName() ); + } + + @NotNull + @Override + protected Stream extractAnnotationsFromRepeatableMappingsAnnotation( + @NotNull PsiAnnotation mappings) { + return extractSubclassMappingAnnotations( mappings ); + } + + private static class ChangeTargetQuickFix extends LocalQuickFixOnPsiElement { + + private final String myText; + private final String myFamilyName; + + private ChangeTargetQuickFix(@NotNull PsiAnnotationMemberValue element) { + super( element ); + myText = MapStructBundle.message( "intention.change.source.property" ); + myFamilyName = MapStructBundle.message( "inspection.subclass.mapping.source.subclass.already.defined", + element.getText() ); + } + + @Override + public @IntentionName @NotNull String getText() { + return myText; + } + + @Override + public void invoke(@NotNull Project project, @NotNull PsiFile psiFile, @NotNull PsiElement psiElement, + @NotNull PsiElement psiElement1) { + FileEditor selectedEditor = FileEditorManager.getInstance( project ).getSelectedEditor(); + if ( selectedEditor instanceof TextEditor textEditor ) { + Editor editor = textEditor.getEditor(); + + TextRange textRange = ((PsiClassObjectAccessExpression) psiElement).getOperand().getTextRange(); + + editor.getCaretModel().moveToOffset( textRange.getStartOffset() ); + LogicalPosition startPosition = editor.getCaretModel().getLogicalPosition(); + editor.getCaretModel().moveToOffset( textRange.getEndOffset() ); + editor.getCaretModel().setCaretsAndSelections( + Collections.singletonList( new CaretState(startPosition, startPosition, + editor.getCaretModel().getLogicalPosition() ) ) ); + editor.getScrollingModel().scrollToCaret( ScrollType.MAKE_VISIBLE ); + } + } + + @Override + public @IntentionFamilyName @NotNull String getFamilyName() { + return myFamilyName; + } + + @Override + public boolean availableInBatchMode() { + return false; + } + } +} diff --git a/src/main/java/org/mapstruct/intellij/inspection/TargetPropertyMappedMoreThanOnceInspection.java b/src/main/java/org/mapstruct/intellij/inspection/TargetPropertyMappedMoreThanOnceInspection.java index e6b459d6..53b42112 100644 --- a/src/main/java/org/mapstruct/intellij/inspection/TargetPropertyMappedMoreThanOnceInspection.java +++ b/src/main/java/org/mapstruct/intellij/inspection/TargetPropertyMappedMoreThanOnceInspection.java @@ -5,206 +5,86 @@ */ package org.mapstruct.intellij.inspection; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; import java.util.Optional; +import java.util.stream.Stream; -import com.intellij.codeInsight.intention.QuickFixFactory; import com.intellij.codeInspection.LocalQuickFix; -import com.intellij.codeInspection.LocalQuickFixAndIntentionActionOnPsiElement; -import com.intellij.codeInspection.LocalQuickFixOnPsiElement; -import com.intellij.codeInspection.ProblemsHolder; -import com.intellij.codeInspection.util.IntentionFamilyName; -import com.intellij.codeInspection.util.IntentionName; -import com.intellij.openapi.editor.CaretState; -import com.intellij.openapi.editor.Editor; -import com.intellij.openapi.editor.LogicalPosition; -import com.intellij.openapi.editor.ScrollType; -import com.intellij.openapi.fileEditor.FileEditor; -import com.intellij.openapi.fileEditor.FileEditorManager; -import com.intellij.openapi.fileEditor.TextEditor; -import com.intellij.openapi.project.Project; -import com.intellij.openapi.util.TextRange; -import com.intellij.openapi.util.text.Strings; -import com.intellij.psi.JavaElementVisitor; import com.intellij.psi.PsiAnnotation; import com.intellij.psi.PsiAnnotationMemberValue; -import com.intellij.psi.PsiClass; -import com.intellij.psi.PsiElement; -import com.intellij.psi.PsiElementVisitor; -import com.intellij.psi.PsiFile; -import com.intellij.psi.PsiMethod; -import com.intellij.psi.PsiType; -import com.intellij.psi.impl.source.tree.java.PsiAnnotationImpl; +import com.intellij.psi.PsiModifierListOwner; import org.jetbrains.annotations.NotNull; import org.mapstruct.intellij.MapStructBundle; import org.mapstruct.intellij.util.MapStructVersion; -import org.mapstruct.intellij.util.MapstructUtil; -import org.mapstruct.intellij.util.TargetUtils; import static com.intellij.codeInsight.AnnotationUtil.getStringAttributeValue; import static org.mapstruct.intellij.util.MapstructAnnotationUtils.extractMappingAnnotationsFromMappings; +import static org.mapstruct.intellij.util.MapstructAnnotationUtils.findAllDefinedMappingAnnotations; import static org.mapstruct.intellij.util.MapstructUtil.MAPPINGS_ANNOTATION_FQN; import static org.mapstruct.intellij.util.MapstructUtil.MAPPING_ANNOTATION_FQN; -import static org.mapstruct.intellij.util.TargetUtils.getTargetType; /** * @author hduelme */ -public class TargetPropertyMappedMoreThanOnceInspection extends InspectionBase { +public class TargetPropertyMappedMoreThanOnceInspection extends MoreThanOnceMappedAnnotationInspectionBase { + @NotNull @Override - PsiElementVisitor buildVisitorInternal(@NotNull ProblemsHolder holder, boolean isOnTheFly) { - return new TargetPropertyMappedMoreThanOnceInspection.MyJavaElementVisitor( holder, - MapstructUtil.resolveMapStructProjectVersion( holder.getFile() ) ); + protected String getSingleMappingAnnotationFqn() { + return MAPPING_ANNOTATION_FQN; } - private static class MyJavaElementVisitor extends JavaElementVisitor { - private final ProblemsHolder holder; - private final MapStructVersion mapStructVersion; - - private MyJavaElementVisitor(ProblemsHolder holder, MapStructVersion mapStructVersion) { - this.holder = holder; - this.mapStructVersion = mapStructVersion; - } - - @Override - public void visitMethod(PsiMethod method) { - if ( !MapstructUtil.isMapper( method.getContainingClass() ) ) { - return; - } - PsiType targetType = getTargetType( method ); - if ( targetType == null ) { - return; - } - Map> problemMap = new HashMap<>(); - for ( PsiAnnotation psiAnnotation : method.getAnnotations() ) { - String qualifiedName = psiAnnotation.getQualifiedName(); - if ( MAPPING_ANNOTATION_FQN.equals( qualifiedName ) ) { - handleMappingAnnotation( psiAnnotation, problemMap ); - } - else if ( MAPPINGS_ANNOTATION_FQN.equals( qualifiedName ) ) { - extractMappingAnnotationsFromMappings( psiAnnotation ) - .forEach( a -> handleMappingAnnotation( a, problemMap ) ); - } - else { - // Handle annotations containing at least one Mapping annotation - handleAnnotationWithMappingAnnotation( psiAnnotation, problemMap ); - } - } - QuickFixFactory quickFixFactory = QuickFixFactory.getInstance(); - for ( Map.Entry> problem : problemMap.entrySet() ) { - List problemElements = problem.getValue(); - if ( problemElements.size() > 1 ) { - for ( PsiElement problemElement : problemElements ) { - LocalQuickFix[] quickFixes = getLocalQuickFixes( problemElement, quickFixFactory ); - holder.registerProblem( problemElement, - MapStructBundle.message( "inspection.target.property.mapped.more.than.once", - problem.getKey() ), quickFixes ); - } - } - } - } - - private static @NotNull LocalQuickFix[] getLocalQuickFixes(PsiElement problemElement, - QuickFixFactory quickFixFactory) { - List quickFixes = new ArrayList<>(2); - if ( problemElement instanceof PsiAnnotation ) { - quickFixes.add( getDeleteFix( problemElement, quickFixFactory ) ); - } - else if ( problemElement instanceof PsiAnnotationMemberValue problemPsiAnnotationMemberValue ) { - Optional.ofNullable( problemElement.getParent() ).map( PsiElement::getParent ) - .map( PsiElement::getParent ).filter( PsiAnnotation.class::isInstance ) - .ifPresent( annotation -> quickFixes.add( - getDeleteFix( annotation, quickFixFactory ) ) ); - quickFixes.add( new ChangeTargetQuickFix( problemPsiAnnotationMemberValue ) ); - } - return quickFixes.toArray( new LocalQuickFix[]{} ); - } - - private static @NotNull LocalQuickFixAndIntentionActionOnPsiElement getDeleteFix( - @NotNull PsiElement problemElement, @NotNull QuickFixFactory quickFixFactory) { - - String annotationName = PsiAnnotationImpl.getAnnotationShortName( problemElement.getText() ); - return quickFixFactory.createDeleteFix( problemElement, - MapStructBundle.message( "intention.remove.annotation", annotationName ) ); - } - - private void handleAnnotationWithMappingAnnotation(PsiAnnotation psiAnnotation, - Map> problemMap) { - PsiClass annotationClass = psiAnnotation.resolveAnnotationType(); - if ( annotationClass == null ) { - return; - } - TargetUtils.findAllDefinedMappingTargets( annotationClass, mapStructVersion ) - .forEach( target -> - problemMap.computeIfAbsent( target, k -> new ArrayList<>() ).add( psiAnnotation ) ); - } - - private static void handleMappingAnnotation(PsiAnnotation psiAnnotation, - Map> problemMap) { - PsiAnnotationMemberValue value = psiAnnotation.findDeclaredAttributeValue( "target" ); - if ( value != null ) { - String target = getStringAttributeValue( value ); - if ( target != null && !target.equals( "." ) ) { - problemMap.computeIfAbsent( target, k -> new ArrayList<>() ).add( value ); - } - } - } - - private static class ChangeTargetQuickFix extends LocalQuickFixOnPsiElement { + @NotNull + @Override + protected String getRepeatableMappingsAnnotationFqn() { + return MAPPINGS_ANNOTATION_FQN; + } - private final String myText; - private final String myFamilyName; + @NotNull + @Override + protected String getAttributeName() { + return "target"; + } - private ChangeTargetQuickFix(@NotNull PsiAnnotationMemberValue element) { - super( element ); - myText = MapStructBundle.message( "intention.change.target.property" ); - myFamilyName = MapStructBundle.message( "inspection.target.property.mapped.more.than.once", - element.getText() ); - } + @NotNull + @Override + protected Stream findAllDefinedMappings(@NotNull PsiModifierListOwner owner, + @NotNull MapStructVersion mapStructVersion) { + return findAllDefinedMappingAnnotations( owner, mapStructVersion ); + } - @Override - public @IntentionName @NotNull String getText() { - return myText; - } + @Override + protected Optional extractCompareKeyFromAnnotationMember( + @NotNull PsiAnnotationMemberValue annotationMemberValue) { + return Optional.ofNullable( getStringAttributeValue( annotationMemberValue ) ) + .filter( target -> !target.equals( "." ) ); + } - @Override - public void invoke(@NotNull Project project, @NotNull PsiFile psiFile, @NotNull PsiElement psiElement, - @NotNull PsiElement psiElement1) { - FileEditor selectedEditor = FileEditorManager.getInstance( project ).getSelectedEditor(); - if ( selectedEditor instanceof TextEditor textEditor ) { - Editor editor = textEditor.getEditor(); + @Override + protected LocalQuickFix getChangeTargetQuickFix(@NotNull PsiAnnotationMemberValue problemPsiAnnotationMemberValue) { + return new ChangeTargetQuickFix( problemPsiAnnotationMemberValue ); + } - TextRange textRange = psiElement.getTextRange(); - String textOfElement = String.valueOf( editor.getDocument() - .getCharsSequence() - .subSequence( textRange.getStartOffset(), textRange.getEndOffset() ) ); - int targetStart = Strings.indexOf( textOfElement, "\"" ) + 1; - int targetEnd = textOfElement.lastIndexOf( "\"" ); + @Override + protected String getProblemDescription(@NotNull String problemKey) { + return MapStructBundle.message( "inspection.target.property.mapped.more.than.once", problemKey ); + } - editor.getCaretModel().moveToOffset( textRange.getStartOffset() + targetStart ); - LogicalPosition startPosition = editor.getCaretModel().getLogicalPosition(); - editor.getCaretModel().moveToOffset( textRange.getStartOffset() + targetEnd ); - editor.getCaretModel().setCaretsAndSelections( - Collections.singletonList( new CaretState(startPosition, startPosition, - editor.getCaretModel().getLogicalPosition() ) ) ); - editor.getScrollingModel().scrollToCaret( ScrollType.MAKE_VISIBLE ); - } - } + @NotNull + @Override + protected Stream extractAnnotationsFromRepeatableMappingsAnnotation( + @NotNull PsiAnnotation mappings) { + return extractMappingAnnotationsFromMappings( mappings ); + } - @Override - public @IntentionFamilyName @NotNull String getFamilyName() { - return myFamilyName; - } + private static class ChangeTargetQuickFix extends ChangeTargetStringQuickFixBase { - @Override - public boolean availableInBatchMode() { - return false; - } + private ChangeTargetQuickFix(@NotNull PsiAnnotationMemberValue element) { + super( + element, + MapStructBundle.message( "intention.change.target.property" ), + MapStructBundle.message( "inspection.target.property.mapped.more.than.once", + element.getText() ) + ); } } } diff --git a/src/main/java/org/mapstruct/intellij/inspection/ValueMappingSourceMappedMoreThanOnceInspection.java b/src/main/java/org/mapstruct/intellij/inspection/ValueMappingSourceMappedMoreThanOnceInspection.java new file mode 100644 index 00000000..7c6c1068 --- /dev/null +++ b/src/main/java/org/mapstruct/intellij/inspection/ValueMappingSourceMappedMoreThanOnceInspection.java @@ -0,0 +1,86 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at https://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.intellij.inspection; + +import java.util.Optional; +import java.util.stream.Stream; + +import com.intellij.codeInspection.LocalQuickFix; +import com.intellij.psi.PsiAnnotation; +import com.intellij.psi.PsiAnnotationMemberValue; +import com.intellij.psi.PsiModifierListOwner; +import org.jetbrains.annotations.NotNull; +import org.mapstruct.intellij.MapStructBundle; +import org.mapstruct.intellij.util.MapStructVersion; + +import static com.intellij.codeInsight.AnnotationUtil.getStringAttributeValue; +import static org.mapstruct.intellij.util.MapstructAnnotationUtils.extractValueMappingAnnotationsFromMappings; +import static org.mapstruct.intellij.util.MapstructAnnotationUtils.findAllDefinedValueMappingAnnotations; +import static org.mapstruct.intellij.util.MapstructUtil.VALUE_MAPPINGS_ANNOTATION_FQN; +import static org.mapstruct.intellij.util.MapstructUtil.VALUE_MAPPING_ANNOTATION_FQN; + +public class ValueMappingSourceMappedMoreThanOnceInspection extends MoreThanOnceMappedAnnotationInspectionBase { + + @NotNull + @Override + protected String getSingleMappingAnnotationFqn() { + return VALUE_MAPPING_ANNOTATION_FQN; + } + + @NotNull + @Override + protected String getRepeatableMappingsAnnotationFqn() { + return VALUE_MAPPINGS_ANNOTATION_FQN; + } + + @NotNull + @Override + protected String getAttributeName() { + return "source"; + } + + @NotNull + @Override + protected Stream findAllDefinedMappings(@NotNull PsiModifierListOwner owner, + @NotNull MapStructVersion mapStructVersion) { + return findAllDefinedValueMappingAnnotations( owner ); + } + + @Override + protected Optional extractCompareKeyFromAnnotationMember( + @NotNull PsiAnnotationMemberValue annotationMemberValue) { + return Optional.ofNullable( getStringAttributeValue( annotationMemberValue ) ); + } + + @Override + protected LocalQuickFix getChangeTargetQuickFix(@NotNull PsiAnnotationMemberValue problemPsiAnnotationMemberValue) { + return new ChangeTargetQuickFix( problemPsiAnnotationMemberValue ); + } + + @Override + protected String getProblemDescription(@NotNull String problemKey) { + return MapStructBundle.message( "inspection.value.mapping.source.mapped.more.than.once", problemKey ); + } + + @NotNull + @Override + protected Stream extractAnnotationsFromRepeatableMappingsAnnotation( + @NotNull PsiAnnotation mappings) { + return extractValueMappingAnnotationsFromMappings( mappings ); + } + + private static class ChangeTargetQuickFix extends ChangeTargetStringQuickFixBase { + + private ChangeTargetQuickFix(@NotNull PsiAnnotationMemberValue element) { + super( + element, + MapStructBundle.message( "intention.change.source.property" ), + MapStructBundle.message( "inspection.value.mapping.source.mapped.more.than.once", + element.getText() ) + ); + } + } +} diff --git a/src/main/java/org/mapstruct/intellij/util/MapstructAnnotationUtils.java b/src/main/java/org/mapstruct/intellij/util/MapstructAnnotationUtils.java index 3dbec884..daab664c 100644 --- a/src/main/java/org/mapstruct/intellij/util/MapstructAnnotationUtils.java +++ b/src/main/java/org/mapstruct/intellij/util/MapstructAnnotationUtils.java @@ -5,6 +5,7 @@ */ package org.mapstruct.intellij.util; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; @@ -56,6 +57,8 @@ import static com.intellij.codeInsight.intention.AddAnnotationPsiFix.addPhysicalAnnotationTo; import static com.intellij.codeInsight.intention.AddAnnotationPsiFix.removePhysicalAnnotations; import static org.mapstruct.intellij.util.MapstructUtil.MAPPING_ANNOTATION_FQN; +import static org.mapstruct.intellij.util.MapstructUtil.SUBCLASS_MAPPINGS_ANNOTATION_FQN; +import static org.mapstruct.intellij.util.MapstructUtil.SUBCLASS_MAPPING_ANNOTATION_FQN; import static org.mapstruct.intellij.util.MapstructUtil.VALUE_MAPPING_ANNOTATION_FQN; /** @@ -323,10 +326,10 @@ private static Stream findMappingAnnotations(@NotNull PsiModifier return findDirectAndMetaAnnotations( method, new HashSet<>() ).stream(); } - return Stream.of( method.getModifierList() ) - .filter( Objects::nonNull ) - .flatMap( psiModifierList -> Arrays.stream( psiModifierList.getAnnotations() ) ) - .filter( MapstructAnnotationUtils::isMappingAnnotation ); + return Optional.ofNullable( method.getModifierList() ) + .map( PsiModifierList::getAnnotations ).stream() + .flatMap( Arrays::stream ) + .filter( MapstructAnnotationUtils::isMappingAnnotation ); } @NotNull @@ -363,24 +366,30 @@ private static List getResolvedClassesInAnnotationsList(PsiModifierLis return Collections.emptyList(); } - public static Stream findAllDefinedValueMappingAnnotations(@NotNull PsiMethod method) { - Stream valueMappingsAnnotations = Stream.empty(); + public static Stream findAllDefinedValueMappingAnnotations(@NotNull PsiModifierListOwner method) { PsiAnnotation valueMappings = findAnnotation( method, true, MapstructUtil.VALUE_MAPPINGS_ANNOTATION_FQN ); - if ( valueMappings != null ) { - valueMappingsAnnotations = arrayAttributeValues( valueMappings.findDeclaredAttributeValue( null ) ) - .stream() - .filter( MapstructAnnotationUtils::isValueMappingPsiAnnotation ) - .map( PsiAnnotation.class::cast ); - } - + Stream valueMappingsAnnotations = extractValueMappingAnnotationsFromMappings( valueMappings ); Stream valueMappingAnnotations = findValueMappingAnnotations( method ); return Stream.concat( valueMappingAnnotations, valueMappingsAnnotations ); } - private static Stream findValueMappingAnnotations(@NotNull PsiMethod method) { - return Stream.of( method.getModifierList().getAnnotations() ) - .filter( MapstructAnnotationUtils::isValueMappingAnnotation ); + @NotNull + public static Stream extractValueMappingAnnotationsFromMappings(@Nullable PsiAnnotation mappings) { + if ( mappings == null ) { + return Stream.empty(); + } + return arrayAttributeValues( mappings.findDeclaredAttributeValue( null ) ) + .stream() + .filter( MapstructAnnotationUtils::isValueMappingPsiAnnotation ) + .map( PsiAnnotation.class::cast ); + } + + private static Stream findValueMappingAnnotations(@NotNull PsiModifierListOwner method) { + return Optional.ofNullable( method.getModifierList() ) + .map( PsiModifierList::getAnnotations ).stream() + .flatMap( Arrays::stream ) + .filter( MapstructAnnotationUtils::isValueMappingAnnotation ); } /** @@ -423,6 +432,46 @@ private static boolean isMappingAnnotation(PsiAnnotation psiAnnotation) { return MAPPING_ANNOTATION_FQN.equals( psiAnnotation.getQualifiedName() ); } + /** + * @param memberValue that needs to be checked + * + * @return {@code true} if the {@code memberValue} is the {@link org.mapstruct.SubclassMapping} + * {@link PsiAnnotation}, {@code false} otherwise + */ + public static boolean isSubclassMappingPsiAnnotation(PsiAnnotationMemberValue memberValue) { + return memberValue instanceof PsiAnnotation psiAnnotation && isSubclassMappingAnnotation( psiAnnotation ); + } + + /** + * @param psiAnnotation that needs to be checked + * + * @return {@code true} if the {@code psiAnnotation} is the {@link org.mapstruct.SubclassMapping} annotation, + * {@code false} otherwise + */ + private static boolean isSubclassMappingAnnotation(PsiAnnotation psiAnnotation) { + return SUBCLASS_MAPPING_ANNOTATION_FQN.equals( psiAnnotation.getQualifiedName() ); + } + + /** + * @param memberValue that needs to be checked + * + * @return {@code true} if the {@code memberValue} is the {@link org.mapstruct.SubclassMappings} + * {@link PsiAnnotation}, {@code false} otherwise + */ + public static boolean isSubclassMappingsPsiAnnotation(PsiAnnotationMemberValue memberValue) { + return memberValue instanceof PsiAnnotation psiAnnotation && isSubclassMappingsAnnotation( psiAnnotation ); + } + + /** + * @param psiAnnotation that needs to be checked + * + * @return {@code true} if the {@code psiAnnotation} is the {@link org.mapstruct.SubclassMappings} annotation, + * {@code false} otherwise + */ + private static boolean isSubclassMappingsAnnotation(PsiAnnotation psiAnnotation) { + return SUBCLASS_MAPPINGS_ANNOTATION_FQN.equals( psiAnnotation.getQualifiedName() ); + } + /** * Find the mapper config reference class or interface defined in the {@code mapperAnnotation} * @@ -640,4 +689,63 @@ else if ( psiAnnotationParentParentParent instanceof PsiAnnotationParamListImpl return null; } + @NotNull + public static Stream findAllDefinedSubclassMappingAnnotations(@NotNull PsiModifierListOwner owner, + boolean includeMetaAnnotations) { + if ( includeMetaAnnotations ) { + return findDirectAndMetaAnnotations2( owner, new HashSet<>() ) + .stream().flatMap( MapstructAnnotationUtils::extractSubclassMappingAnnotations ); + } + + return Arrays.stream( owner.getAnnotations() ) + .flatMap( MapstructAnnotationUtils::extractSubclassMappingAnnotations ); + } + + @NotNull + public static Stream extractSubclassMappingAnnotations(PsiAnnotation annotation) { + if ( isSubclassMappingPsiAnnotation( annotation ) ) { + return Stream.of( annotation ); + } + if ( isSubclassMappingsPsiAnnotation( annotation ) ) { + return extractSubclassMappingAnnotationsFromSubclassMappings( annotation ); + } + return Stream.empty(); + } + + @NotNull + private static Stream extractSubclassMappingAnnotationsFromSubclassMappings( + PsiAnnotation annotation) { + return arrayAttributeValues( annotation.findDeclaredAttributeValue( null ) ) + .stream() + .filter( MapstructAnnotationUtils::isSubclassMappingPsiAnnotation ) + .map( PsiAnnotation.class::cast ); + } + + @NotNull + private static List findDirectAndMetaAnnotations2(@NotNull PsiModifierListOwner owner, + Set visited) { + + List result = new ArrayList<>(); + + // to avoid infinite loops, do not include meta annotations at this point + findAllDefinedSubclassMappingAnnotations( owner, false ).forEach( result::add ); + + result.addAll( findSubclassMetaAnnotations( owner, visited ) ); + + return result; + } + + public static List findSubclassMetaAnnotations( @NotNull PsiModifierListOwner owner, + Set visited ) { + List result = new ArrayList<>(); + List annotationClasses = getResolvedClassesInAnnotationsList( owner ); + + for ( PsiClass annotationClass : annotationClasses ) { + if ( visited.add( annotationClass ) ) { + result.addAll( findDirectAndMetaAnnotations2( annotationClass, visited ) ); + } + } + return result; + } + } diff --git a/src/main/java/org/mapstruct/intellij/util/MapstructUtil.java b/src/main/java/org/mapstruct/intellij/util/MapstructUtil.java index 1ec910ee..aa3cbd3c 100644 --- a/src/main/java/org/mapstruct/intellij/util/MapstructUtil.java +++ b/src/main/java/org/mapstruct/intellij/util/MapstructUtil.java @@ -61,6 +61,8 @@ import org.mapstruct.MappingTarget; import org.mapstruct.Mappings; import org.mapstruct.Named; +import org.mapstruct.SubclassMapping; +import org.mapstruct.SubclassMappings; import org.mapstruct.ValueMapping; import org.mapstruct.ValueMappings; import org.mapstruct.factory.Mappers; @@ -98,8 +100,11 @@ public class MapstructUtil { public static final String MAPPINGS_ANNOTATION_FQN = Mappings.class.getName(); - static final String VALUE_MAPPING_ANNOTATION_FQN = ValueMapping.class.getName(); - static final String VALUE_MAPPINGS_ANNOTATION_FQN = ValueMappings.class.getName(); + public static final String SUBCLASS_MAPPING_ANNOTATION_FQN = SubclassMapping.class.getName(); + public static final String SUBCLASS_MAPPINGS_ANNOTATION_FQN = SubclassMappings.class.getName(); + + public static final String VALUE_MAPPING_ANNOTATION_FQN = ValueMapping.class.getName(); + public static final String VALUE_MAPPINGS_ANNOTATION_FQN = ValueMappings.class.getName(); private static final String MAPPING_TARGET_ANNOTATION_FQN = MappingTarget.class.getName(); private static final String CONTEXT_ANNOTATION_FQN = Context.class.getName(); private static final String BUILDER_ANNOTATION_FQN = Builder.class.getName(); diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index b111da73..90da4ee1 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -155,6 +155,22 @@ key="inspection.mapstruct.references" shortName="MapstructReferenceInspection" implementationClass="org.mapstruct.intellij.inspection.MapstructReferenceInspection"/> + + diff --git a/src/main/resources/inspectionDescriptions/SubclassMappingSourceSubclassMappedMoreThanOnceInspection.html b/src/main/resources/inspectionDescriptions/SubclassMappingSourceSubclassMappedMoreThanOnceInspection.html new file mode 100644 index 00000000..8091be39 --- /dev/null +++ b/src/main/resources/inspectionDescriptions/SubclassMappingSourceSubclassMappedMoreThanOnceInspection.html @@ -0,0 +1,31 @@ + + +

    + This inspection reports when a subclass is mapped more than once +

    +

    +

    
    +//wrong
    +@Mapper
    +public interface FruitMapper {
    +    @SubclassMapping( source = AppleDto.class, target = Apple.class )
    +    @SubclassMapping( source = AppleDto.class, target = Fruit.class )
    +    @SubclassMapping( source = BananaDto.class, target = Banana.class )
    +    Fruit map( FruitDto source );
    +}
    +
    +

    +

    +

    
    +//correct
    +@Mapper
    +public interface FruitMapper {
    +    @SubclassMapping( source = AppleDto.class, target = Apple.class )
    +    @SubclassMapping( source = BananaDto.class, target = Banana.class )
    +    Fruit map( FruitDto source );
    +}
    +
    +

    + + + diff --git a/src/main/resources/inspectionDescriptions/ValueMappingSourceMappedMoreThanOnceInspection.html b/src/main/resources/inspectionDescriptions/ValueMappingSourceMappedMoreThanOnceInspection.html new file mode 100644 index 00000000..d7e75c70 --- /dev/null +++ b/src/main/resources/inspectionDescriptions/ValueMappingSourceMappedMoreThanOnceInspection.html @@ -0,0 +1,33 @@ + + +

    + This inspection reports when a value mapping source property is mapped more than once +

    +

    +

    
    +//wrong
    +@Mapper
    +public interface OrderMapper {
    +
    +    @ValueMapping(target = "SPECIAL", source = "EXTRA")
    +    @ValueMapping(target = "DEFAULT", source = "EXTRA")
    +    @ValueMapping(target = "DEFAULT", source = "NORMAL")
    +    ExternalOrderType orderTypeToExternalOrderType(OrderType orderType);
    +}
    +
    +

    +

    +

    
    +//correct
    +@Mapper
    +public interface OrderMapper {
    +
    +    @ValueMapping(target = "SPECIAL", source = "EXTRA")
    +    @ValueMapping(target = "DEFAULT", source = "NORMAL")
    +    Fruit map( FruitDto source );
    +}
    +
    +

    + + + diff --git a/src/main/resources/org/mapstruct/intellij/messages/MapStructBundle.properties b/src/main/resources/org/mapstruct/intellij/messages/MapStructBundle.properties index 9a5608bd..9ea7e638 100644 --- a/src/main/resources/org/mapstruct/intellij/messages/MapStructBundle.properties +++ b/src/main/resources/org/mapstruct/intellij/messages/MapStructBundle.properties @@ -29,6 +29,10 @@ inspection.target.property.mapped.more.than.once=Target property ''{0}'' must no inspection.target.property.mapped.more.than.once.title=Target properties must not be mapped more than once. inspection.source.property.this.used=''.'' should not be used as a source. inspection.mapstruct.references=Injected mapstruct references +inspection.subclass.mapping.source.subclass.already.defined=Subclass ''{0}'' is already defined as a source. +inspection.subclass.mapping.source.subclass.already.defined.title=Subclass source property is already defined as a source. +inspection.value.mapping.source.mapped.more.than.once=Value mapping source property ''{0}'' must not be mapped more than once. +inspection.value.mapping.source.mapped.more.than.once.title=Value mapping source property must not be mapped more than once. intention.add.ignore.all.unmapped.target.properties=Add ignore all unmapped target properties intention.add.ignore.unmapped.target.property=Add ignore unmapped target property intention.add.unmapped.target.property=Add unmapped target property @@ -40,6 +44,7 @@ intention.wrong.map.mapping.map.type.raw=Add type to Map for mapping Map to Bean intention.wrong.map.mapping.map.key=Use Map with key of type String for mapping Map to Bean intention.remove.annotation=Remove {0} annotation intention.change.target.property=Change target property +intention.change.source.property=Change source property intention.replace.source.property=Replace source ''.'' with ''{0}'' plugin.settings.title=MapStruct plugin.settings.quickFix.title=Quick fix properties diff --git a/src/test/java/org/mapstruct/intellij/inspection/SubclassMappingSourceSubclassMappedMoreThanOnceInspectionTest.java b/src/test/java/org/mapstruct/intellij/inspection/SubclassMappingSourceSubclassMappedMoreThanOnceInspectionTest.java new file mode 100644 index 00000000..c87168cd --- /dev/null +++ b/src/test/java/org/mapstruct/intellij/inspection/SubclassMappingSourceSubclassMappedMoreThanOnceInspectionTest.java @@ -0,0 +1,68 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at https://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.intellij.inspection; + +import java.util.List; + +import com.intellij.codeInsight.intention.IntentionAction; +import com.intellij.codeInspection.LocalInspectionTool; +import com.intellij.openapi.editor.Caret; +import org.jetbrains.annotations.NotNull; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author hduelme + */ +public class SubclassMappingSourceSubclassMappedMoreThanOnceInspectionTest extends BaseInspectionTest { + + @Override + protected @NotNull Class getInspection() { + return SubclassMappingSourceSubclassMappedMoreThanOnceInspection.class; + } + + public void testSubclassMappingSourceSubclassMappedMoreThanOnce() { + doTest(); + String testName = getTestName( false ); + List allQuickFixes = myFixture.getAllQuickFixes(); + + assertThat( allQuickFixes ) + .extracting( IntentionAction::getText ) + .as( "Intent Text" ) + .containsExactly( + "Remove SubclassMapping annotation", + "Change source property", + "Remove SubclassMapping annotation", + "Change source property", + "Remove SubclassMapping annotation", + "Change source property", + "Remove SubclassMapping annotation", + "Change source property", + "Remove SubclassMapping annotation", + "Change source property", + "Remove SubclassMapping annotation", + "Change source property", + "Remove SubclassMapping annotation", + "Change source property", + "Remove MySubclassMappingAnnotation annotation", + "Remove SubclassMapping annotation", + "Change source property", + "Remove MySubclassMappingAnnotation annotation" + ); + + // Delete annotations + myFixture.launchAction( allQuickFixes.get( 0 ) ); + myFixture.launchAction( allQuickFixes.get( 3 ) ); + myFixture.launchAction( allQuickFixes.get( 6 ) ); + myFixture.launchAction( allQuickFixes.get( 8 ) ); + myFixture.launchAction( allQuickFixes.get( 14 ) ); + // Set cursor + myFixture.launchAction( allQuickFixes.get( 16 ) ); + myFixture.checkResultByFile( testName + "_after.java" ); + Caret currentCaret = myFixture.getEditor().getCaretModel().getCurrentCaret(); + assertThat( currentCaret.getSelectedText( ) ).isEqualTo( "SourceSubclass1" ); + } +} diff --git a/src/test/java/org/mapstruct/intellij/inspection/ValueMappingSourceMappedMoreThanOnceInspectionTest.java b/src/test/java/org/mapstruct/intellij/inspection/ValueMappingSourceMappedMoreThanOnceInspectionTest.java new file mode 100644 index 00000000..1529f727 --- /dev/null +++ b/src/test/java/org/mapstruct/intellij/inspection/ValueMappingSourceMappedMoreThanOnceInspectionTest.java @@ -0,0 +1,68 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at https://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.intellij.inspection; + +import java.util.List; + +import com.intellij.codeInsight.intention.IntentionAction; +import com.intellij.codeInspection.LocalInspectionTool; +import com.intellij.openapi.editor.Caret; +import org.jetbrains.annotations.NotNull; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author hduelme + */ +public class ValueMappingSourceMappedMoreThanOnceInspectionTest extends BaseInspectionTest { + + @Override + protected @NotNull Class getInspection() { + return ValueMappingSourceMappedMoreThanOnceInspection.class; + } + + public void testValueMappingSourceMappedMoreThanOnce() { + doTest(); + String testName = getTestName( false ); + List allQuickFixes = myFixture.getAllQuickFixes(); + + assertThat( allQuickFixes ) + .extracting( IntentionAction::getText ) + .as( "Intent Text" ) + .containsExactly( + "Remove ValueMapping annotation", + "Change source property", + "Remove ValueMapping annotation", + "Change source property", + "Remove ValueMapping annotation", + "Change source property", + "Remove ValueMapping annotation", + "Change source property", + "Remove ValueMapping annotation", + "Change source property", + "Remove ValueMapping annotation", + "Change source property", + "Remove ValueMapping annotation", + "Change source property", + "Remove MyValueMappingAnnotation annotation", + "Remove ValueMapping annotation", + "Change source property", + "Remove MyValueMappingAnnotation annotation" + ); + + // Delete annotations + myFixture.launchAction( allQuickFixes.get( 0 ) ); + myFixture.launchAction( allQuickFixes.get( 3 ) ); + myFixture.launchAction( allQuickFixes.get( 6 ) ); + myFixture.launchAction( allQuickFixes.get( 8 ) ); + myFixture.launchAction( allQuickFixes.get( 14 ) ); + // Set cursor + myFixture.launchAction( allQuickFixes.get( 16 ) ); + myFixture.checkResultByFile( testName + "_after.java" ); + Caret currentCaret = myFixture.getEditor().getCaretModel().getCurrentCaret(); + assertThat( currentCaret.getSelectedText( ) ).isEqualTo( "A" ); + } +} diff --git a/testData/inspection/SubclassMappingSourceSubclassMappedMoreThanOnce.java b/testData/inspection/SubclassMappingSourceSubclassMappedMoreThanOnce.java new file mode 100644 index 00000000..b637e902 --- /dev/null +++ b/testData/inspection/SubclassMappingSourceSubclassMappedMoreThanOnce.java @@ -0,0 +1,137 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at https://www.apache.org/licenses/LICENSE-2.0 + */ +package org.example.data; + +import org.mapstruct.Mapper; +import org.mapstruct.SubclassMapping; +import org.mapstruct.SubclassMappings; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +class Source { + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} + +class SourceSubclass1 extends Source { + private String type; + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } +} + +class SourceSubclass2 extends Source { + private String category; + + public String getCategory() { + return category; + } + + public void setCategory(String category) { + this.category = category; + } +} + +class Target { + private String testName; + + public String getTestName() { + return testName; + } + + public void setTestName(String testName) { + this.testName = testName; + } +} + +class TargetSubclass1 extends Target { + private String kind; + + public String getKind() { + return kind; + } + + public void setKind(String kind) { + this.kind = kind; + } +} + +class TargetSubclass2 extends Target { + private String label; + + public String getLabel() { + return label; + } + + public void setLabel(String label) { + this.label = label; + } +} + +@Retention(RetentionPolicy.CLASS) +@SubclassMapping(source = SourceSubclass1.class, target = TargetSubclass1.class) +@interface MySubclassMappingAnnotation { +} + + +@Mapper +interface SubclassMappingSourceMappedMoreThanOnceBySubclassMappingAnnotationMapper { + + @SubclassMapping(source = SourceSubclass1.class, target = TargetSubclass1.class) + @SubclassMapping(source = SourceSubclass1.class, target = TargetSubclass2.class) + Target map(Source source); +} + +@Mapper +interface SubclassMappingSourceMappedMoreThanOnceBySubclassMappingsAnnotationMapper { + + @SubclassMappings({ + @SubclassMapping(source = SourceSubclass1.class, target = TargetSubclass1.class), + @SubclassMapping(source = SourceSubclass1.class, target = TargetSubclass2.class) + }) + Target map(Source source); +} + +@Mapper +interface SubclassMappingSourceMappedMoreThanOnceBySubclassMappingAnnotationAndSubclassMappingsAnnotationMapper { + + @SubclassMapping(source = SourceSubclass1.class, target = TargetSubclass1.class) + @SubclassMappings({ + @SubclassMapping(source = SourceSubclass1.class, target = TargetSubclass2.class) + }) + Target map(Source source); +} + +@Mapper +interface SubclassMappingSourceMappedMoreThanOnceByMySubclassMappingAnnotationAndSubclassMappingAnnotationMapper { + + @SubclassMapping(source = SourceSubclass1.class, target = TargetSubclass2.class) + @MySubclassMappingAnnotation + Target map(Source source); +} + +@Mapper +interface SubclassMappingSourceMappedMoreThanOnceByMySubclassMappingAnnotationAndSubclassMappingsAnnotationMapper { + + @SubclassMappings({ + @SubclassMapping(source = SourceSubclass1.class, target = TargetSubclass2.class) + }) + @MySubclassMappingAnnotation + Target map(Source source); +} diff --git a/testData/inspection/SubclassMappingSourceSubclassMappedMoreThanOnce_after.java b/testData/inspection/SubclassMappingSourceSubclassMappedMoreThanOnce_after.java new file mode 100644 index 00000000..4e42d35a --- /dev/null +++ b/testData/inspection/SubclassMappingSourceSubclassMappedMoreThanOnce_after.java @@ -0,0 +1,133 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at https://www.apache.org/licenses/LICENSE-2.0 + */ +package org.example.data; + +import org.mapstruct.Mapper; +import org.mapstruct.SubclassMapping; +import org.mapstruct.SubclassMappings; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +class Source { + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} + +class SourceSubclass1 extends Source { + private String type; + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } +} + +class SourceSubclass2 extends Source { + private String category; + + public String getCategory() { + return category; + } + + public void setCategory(String category) { + this.category = category; + } +} + +class Target { + private String testName; + + public String getTestName() { + return testName; + } + + public void setTestName(String testName) { + this.testName = testName; + } +} + +class TargetSubclass1 extends Target { + private String kind; + + public String getKind() { + return kind; + } + + public void setKind(String kind) { + this.kind = kind; + } +} + +class TargetSubclass2 extends Target { + private String label; + + public String getLabel() { + return label; + } + + public void setLabel(String label) { + this.label = label; + } +} + +@Retention(RetentionPolicy.CLASS) +@SubclassMapping(source = SourceSubclass1.class, target = TargetSubclass1.class) +@interface MySubclassMappingAnnotation { +} + + +@Mapper +interface SubclassMappingSourceMappedMoreThanOnceBySubclassMappingAnnotationMapper { + + @SubclassMapping(source = SourceSubclass1.class, target = TargetSubclass2.class) + Target map(Source source); +} + +@Mapper +interface SubclassMappingSourceMappedMoreThanOnceBySubclassMappingsAnnotationMapper { + + @SubclassMappings({ + @SubclassMapping(source = SourceSubclass1.class, target = TargetSubclass1.class) + }) + Target map(Source source); +} + +@Mapper +interface SubclassMappingSourceMappedMoreThanOnceBySubclassMappingAnnotationAndSubclassMappingsAnnotationMapper { + + @SubclassMappings({ + @SubclassMapping(source = SourceSubclass1.class, target = TargetSubclass2.class) + }) + Target map(Source source); +} + +@Mapper +interface SubclassMappingSourceMappedMoreThanOnceByMySubclassMappingAnnotationAndSubclassMappingAnnotationMapper { + + @SubclassMapping(source = SourceSubclass1.class, target = TargetSubclass2.class) + Target map(Source source); +} + +@Mapper +interface SubclassMappingSourceMappedMoreThanOnceByMySubclassMappingAnnotationAndSubclassMappingsAnnotationMapper { + + @SubclassMappings({ + @SubclassMapping(source = SourceSubclass1.class, target = TargetSubclass2.class) + }) + @MySubclassMappingAnnotation + Target map(Source source); +} diff --git a/testData/inspection/ValueMappingSourceMappedMoreThanOnce.java b/testData/inspection/ValueMappingSourceMappedMoreThanOnce.java new file mode 100644 index 00000000..8fb6def4 --- /dev/null +++ b/testData/inspection/ValueMappingSourceMappedMoreThanOnce.java @@ -0,0 +1,76 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at https://www.apache.org/licenses/LICENSE-2.0 + */ + +import org.mapstruct.Mapper; +import org.mapstruct.ValueMapping; +import org.mapstruct.ValueMappings; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +enum Target { + A, + B, +} + +enum Source { + A, + B, +} + + + +@Retention(RetentionPolicy.CLASS) +@ValueMapping(target = "B", source = "A") +@interface MyValueMappingAnnotation { +} + + +@Mapper +interface ValueMappingSourceMappedMoreThanOnceByMappingAnnotationMapper { + + @ValueMapping(target = "A", source = "A") + @ValueMapping(target = "B", source = "A") + Target map(Source source); +} + +@Mapper +interface ValueMappingSourceMappedMoreThanOnceByMappingsAnnotationsMapper { + + @ValueMappings({ + @ValueMapping(target = "A", source = "A"), + @ValueMapping(target = "B", source = "A") + }) + Target map(Source source); +} + +@Mapper +interface ValueMappingSourceMappedMoreThanOnceByMappingsAnnotationsAndMappingAnnotationMapper { + + @ValueMapping(target = "A", source = "A") + @ValueMappings({ + @ValueMapping(target = "B", source = "A") + }) + Target map(Source source); +} + +@Mapper +interface ValueMappingSourceMappedMoreThanOnceByMyMappingAnnotationAndMappingAnnotationMapper { + + @ValueMapping(target = "A", source = "A") + @MyValueMappingAnnotation + Target map(Source source); +} + +@Mapper +interface ValueMappingSourceMappedMoreThanOnceByMyMappingAnnotationAndMappingsAnnotationMapper { + + @ValueMappings({ + @ValueMapping(target = "A", source = "A") + }) + @MyValueMappingAnnotation + Target map(Source source); +} \ No newline at end of file diff --git a/testData/inspection/ValueMappingSourceMappedMoreThanOnce_after.java b/testData/inspection/ValueMappingSourceMappedMoreThanOnce_after.java new file mode 100644 index 00000000..83fbbce6 --- /dev/null +++ b/testData/inspection/ValueMappingSourceMappedMoreThanOnce_after.java @@ -0,0 +1,72 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at https://www.apache.org/licenses/LICENSE-2.0 + */ + +import org.mapstruct.Mapper; +import org.mapstruct.ValueMapping; +import org.mapstruct.ValueMappings; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +enum Target { + A, + B, +} + +enum Source { + A, + B, +} + + + +@Retention(RetentionPolicy.CLASS) +@ValueMapping(target = "B", source = "A") +@interface MyValueMappingAnnotation { +} + + +@Mapper +interface ValueMappingSourceMappedMoreThanOnceByMappingAnnotationMapper { + + @ValueMapping(target = "B", source = "A") + Target map(Source source); +} + +@Mapper +interface ValueMappingSourceMappedMoreThanOnceByMappingsAnnotationsMapper { + + @ValueMappings({ + @ValueMapping(target = "A", source = "A") + }) + Target map(Source source); +} + +@Mapper +interface ValueMappingSourceMappedMoreThanOnceByMappingsAnnotationsAndMappingAnnotationMapper { + + @ValueMappings({ + @ValueMapping(target = "B", source = "A") + }) + Target map(Source source); +} + +@Mapper +interface ValueMappingSourceMappedMoreThanOnceByMyMappingAnnotationAndMappingAnnotationMapper { + + @ValueMapping(target = "A", source = "A") + Target map(Source source); +} + +@Mapper +interface ValueMappingSourceMappedMoreThanOnceByMyMappingAnnotationAndMappingsAnnotationMapper { + + @ValueMappings({ + @ValueMapping(target = "A", source = "A") + }) + @MyValueMappingAnnotation + Target map(Source source); +} \ No newline at end of file