Skip to content

Commit

Permalink
#153 additional inspection for @mapping annotation (e.g.multiple or n…
Browse files Browse the repository at this point in the history
…o sources)
  • Loading branch information
hduelme authored Oct 20, 2023
1 parent f82fc92 commit 14393d1
Show file tree
Hide file tree
Showing 33 changed files with 1,751 additions and 3 deletions.
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,10 @@ To learn more about MapStruct have a look at the [mapstruct](https://github.com/
* `@Mapper` or `@MapperConfig` annotation missing
* Unmapped target properties with quick fixes: Add unmapped target property and Ignore unmapped target property.
Uses `unmappedTargetPolicy` to determine the severity that should be used

* No `source` defined in `@Mapping` annotation
* More than one `source` in `@Mapping` annotation defined with quick fixes: Remove `source`. Remove `constant`. Remove `expression`. Use `constant` as `defaultValue`. Use `expression` as `defaultExpression`.
* More than one default source in `@Mapping` annotation defined with quick fixes: Remove `defaultValue`. Remove `defaultExpression`.

## Requirements

The MapStruct plugin requires Java 11 or later
Expand Down
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ task libs(type: Sync) {
rename 'mapstruct-1.5.3.Final.jar', 'mapstruct.jar'
}

def mockJdkLocation = "https://github.com/JetBrains/intellij-community/raw/master/java/mock"
def mockJdkLocation = "https://github.com/JetBrains/intellij-community/raw/212.5712/java/mock"
def mockJdkDest = "$buildDir/mock"
def downloadMockJdk(mockJdkLocation, mockJdkDest, mockJdkVersion) {
def location = mockJdkLocation + mockJdkVersion
Expand Down
3 changes: 3 additions & 0 deletions description.html
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@
<li><code>@Mapper</code> or <code>@MapperConfig</code> annotation missing</li>
<li>Unmapped target properties with quick fixes: Add unmapped target property and Ignore unmapped target property.
Uses <code>unmappedTargetPolicy</code> to determine the severity that should be used</li>
<li>No <code>source</code> defined in <code>@Mapping</code> annotation</li>
<li>More than one <code>source</code> in <code>@Mapping</code> annotation defined with quick fixes: Remove <code>source</code>. Remove <code>constant</code>. Remove <code>expression</code>. Use <code>constant</code> as <code>defaultValue</code>. Use <code>expression</code> as <code>defaultExpression</code>.</li>
<li>More than one default source in <code>@Mapping</code> annotation defined with quick fixes: Remove <code>defaultValue</code>. Remove <code>defaultExpression</code>.</li>
</ul>
</li>
</ul>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,283 @@
/*
* 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 com.intellij.codeInspection.LocalQuickFixOnPsiElement;
import com.intellij.codeInspection.ProblemsHolder;
import com.intellij.codeInspection.util.IntentionFamilyName;
import com.intellij.codeInspection.util.IntentionName;
import com.intellij.lang.jvm.annotation.JvmAnnotationAttribute;
import com.intellij.openapi.project.Project;
import com.intellij.psi.JavaElementVisitor;
import com.intellij.psi.PsiAnnotation;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiElementVisitor;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiNameValuePair;
import com.intellij.psi.impl.source.tree.java.PsiAnnotationParamListImpl;
import org.jetbrains.annotations.NotNull;
import org.mapstruct.intellij.util.MapstructUtil;

import static com.intellij.psi.PsiElementFactory.getInstance;

public abstract class MappingAnnotationInspectionBase extends InspectionBase {

@Override
@NotNull PsiElementVisitor buildVisitorInternal( @NotNull ProblemsHolder holder, boolean isOnTheFly ) {
return new MappingAnnotationInspectionBase.MyJavaElementVisitor( holder );
}

private class MyJavaElementVisitor extends JavaElementVisitor {
private final ProblemsHolder problemsHolder;

private MyJavaElementVisitor( ProblemsHolder problemsHolder ) {
this.problemsHolder = problemsHolder;
}

@Override
public void visitAnnotation( PsiAnnotation annotation ) {
super.visitAnnotation( annotation );
if (annotation.hasQualifiedName( MapstructUtil.MAPPING_ANNOTATION_FQN )) {
MappingAnnotation mappingAnnotation = new MappingAnnotation();
for (JvmAnnotationAttribute annotationAttribute : annotation.getAttributes()) {
// exclude not written attributes. They result in a syntax error
if (annotationAttribute instanceof PsiNameValuePair
&& annotationAttribute.getAttributeValue() != null) {
PsiNameValuePair nameValuePair = (PsiNameValuePair) annotationAttribute;
switch (nameValuePair.getAttributeName()) {
case "source":
mappingAnnotation.setSourceProperty( nameValuePair );
break;
case "constant":
mappingAnnotation.setConstantProperty( nameValuePair );
break;
case "expression":
mappingAnnotation.setExpressionProperty( nameValuePair );
break;
case "defaultValue":
mappingAnnotation.setDefaultValueProperty( nameValuePair );
break;
case "defaultExpression":
mappingAnnotation.setDefaultExpressionProperty( nameValuePair );
break;
case "ignore":
mappingAnnotation.setIgnoreProperty( nameValuePair );
break;
case "dependsOn":
mappingAnnotation.setDependsOnProperty( nameValuePair );
break;
case "qualifiedByName":
mappingAnnotation.setQualifiedByNameProperty( nameValuePair );
break;
default:
break;
}
}
}

visitMappingAnnotation( problemsHolder, annotation, mappingAnnotation );
}
}

}

abstract void visitMappingAnnotation( @NotNull ProblemsHolder problemsHolder, @NotNull PsiAnnotation psiAnnotation,
@NotNull MappingAnnotation mappingAnnotation );

protected static class MappingAnnotation {
private PsiNameValuePair sourceProperty;
private PsiNameValuePair constantProperty;
private PsiNameValuePair defaultValueProperty;
private PsiNameValuePair expressionProperty;
private PsiNameValuePair defaultExpressionProperty;
private PsiNameValuePair ignoreProperty;
private PsiNameValuePair dependsOnProperty;
private PsiNameValuePair qualifiedByNameProperty;

public PsiNameValuePair getSourceProperty() {
return sourceProperty;
}

public void setSourceProperty( PsiNameValuePair sourceProperty ) {
this.sourceProperty = sourceProperty;
}

public PsiNameValuePair getConstantProperty() {
return constantProperty;
}

public void setConstantProperty( PsiNameValuePair constantProperty ) {
this.constantProperty = constantProperty;
}

public PsiNameValuePair getDefaultValueProperty() {
return defaultValueProperty;
}

public void setDefaultValueProperty( PsiNameValuePair defaultValueProperty ) {
this.defaultValueProperty = defaultValueProperty;
}

public PsiNameValuePair getExpressionProperty() {
return expressionProperty;
}

public void setExpressionProperty( PsiNameValuePair expressionProperty ) {
this.expressionProperty = expressionProperty;
}

public PsiNameValuePair getDefaultExpressionProperty() {
return defaultExpressionProperty;
}

public void setDefaultExpressionProperty( PsiNameValuePair defaultExpressionProperty ) {
this.defaultExpressionProperty = defaultExpressionProperty;
}

public PsiNameValuePair getIgnoreProperty() {
return ignoreProperty;
}

public void setIgnoreProperty( PsiNameValuePair ignoreProperty ) {
this.ignoreProperty = ignoreProperty;
}

public boolean hasNoSourceProperties() {
return sourceProperty == null && defaultValueProperty == null && expressionProperty == null
&& ignoreProperty == null && constantProperty == null && dependsOnProperty == null
&& qualifiedByNameProperty == null;
}

public boolean hasNoDefaultProperties() {
return defaultValueProperty == null && defaultExpressionProperty == null;
}

public PsiNameValuePair getDependsOnProperty() {
return dependsOnProperty;
}

public void setDependsOnProperty(PsiNameValuePair dependsOnProperty) {
this.dependsOnProperty = dependsOnProperty;
}

public PsiNameValuePair getQualifiedByNameProperty() {
return qualifiedByNameProperty;
}

public void setQualifiedByNameProperty(PsiNameValuePair qualifiedByNameProperty) {
this.qualifiedByNameProperty = qualifiedByNameProperty;
}
}

protected static RemoveAnnotationAttributeQuickFix createRemoveAnnotationAttributeQuickFix(
@NotNull PsiNameValuePair annotationAttribute, @NotNull String text, @NotNull String family ) {
return new RemoveAnnotationAttributeQuickFix( annotationAttribute, text, family );
}

protected static ReplaceAsDefaultValueQuickFix createReplaceAsDefaultValueQuickFix(
@NotNull PsiNameValuePair annotationAttribute, @NotNull String source,
@NotNull String target, @NotNull String text, @NotNull String family ) {
return new ReplaceAsDefaultValueQuickFix( annotationAttribute, source, target, text, family );
}

protected static class RemoveAnnotationAttributeQuickFix extends LocalQuickFixOnPsiElement {
private final String text;
private final String family;

private RemoveAnnotationAttributeQuickFix( @NotNull PsiNameValuePair element, @NotNull String text,
@NotNull String family) {
super( element );
this.text = text;
this.family = family;
}

@Override
public boolean isAvailable( @NotNull Project project, @NotNull PsiFile file, @NotNull PsiElement startElement,
@NotNull PsiElement endElement ) {
return startElement.isValid();
}

@Override
public @IntentionName @NotNull String getText() {
return text;
}

@Override
public void invoke( @NotNull Project project, @NotNull PsiFile file, @NotNull PsiElement startElement,
@NotNull PsiElement endElement ) {
startElement.delete();
}

@Override
public @IntentionFamilyName @NotNull String getFamilyName() {
return family;
}

@Override
public boolean availableInBatchMode() {
return false;
}
}

protected static class ReplaceAsDefaultValueQuickFix extends LocalQuickFixOnPsiElement {

private final String source;
private final String target;
private final String text;
private final String family;

private ReplaceAsDefaultValueQuickFix( @NotNull PsiNameValuePair element, @NotNull String source,
@NotNull String target, @NotNull String text,
@NotNull String family) {
super( element );
this.source = source;
this.target = target;
this.text = text;
this.family = family;
}

@Override
public boolean isAvailable( @NotNull Project project, @NotNull PsiFile file, @NotNull PsiElement startElement,
@NotNull PsiElement endElement ) {
if ( !endElement.isValid() ) {
return false;
}
PsiElement parent = endElement.getParent();
return parent.isValid() && parent instanceof PsiAnnotationParamListImpl;
}

@Override
public @IntentionName @NotNull String getText() {
return text;
}

@Override
public void invoke( @NotNull Project project, @NotNull PsiFile file, @NotNull PsiElement startElement,
@NotNull PsiElement endElement ) {
if (endElement instanceof PsiNameValuePair) {
PsiNameValuePair end = (PsiNameValuePair) endElement;
PsiAnnotationParamListImpl parent = (PsiAnnotationParamListImpl) end.getParent();
PsiElement parent1 = parent.getParent();

// don't replace inside of strings. Only the constant value name
String annotationText = parent1.getText().replaceFirst( "(?<!\")\\s*,?\\s*" + source + "\\s*=\\s*",
target + " = " );
parent1.replace( getInstance( project ).createAnnotationFromText( annotationText, parent1 ) );
}
}

@Override
public @IntentionFamilyName @NotNull String getFamilyName() {
return family;
}

@Override
public boolean availableInBatchMode() {
return false;
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* 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 com.intellij.codeInspection.ProblemsHolder;
import com.intellij.psi.PsiAnnotation;
import org.jetbrains.annotations.NotNull;
import org.mapstruct.intellij.MapStructBundle;

/**
* Inspection that checks if inside a @Mapping annotation more than one default source property is defined
*
* @author hduelme
*/
public class MoreThanOneDefaultSourcePropertyDefinedInspection extends MappingAnnotationInspectionBase {

@Override
void visitMappingAnnotation( @NotNull ProblemsHolder problemsHolder, @NotNull PsiAnnotation psiAnnotation,
@NotNull MappingAnnotation mappingAnnotation ) {
// only apply if source property is defined
if (mappingAnnotation.getSourceProperty() != null && mappingAnnotation.getDefaultValueProperty() != null
&& mappingAnnotation.getDefaultExpressionProperty() != null) {
String family = MapStructBundle.message( "intention.more.than.one.default.source.property" );
problemsHolder.registerProblem( psiAnnotation,
MapStructBundle.message( "inspection.more.than.one.default.source.property" ),
createRemoveAnnotationAttributeQuickFix( mappingAnnotation.getDefaultValueProperty(),
"Remove default value", family ),
createRemoveAnnotationAttributeQuickFix( mappingAnnotation.getDefaultExpressionProperty(),
"Remove default expression", family ) );

}
}

}
Loading

0 comments on commit 14393d1

Please sign in to comment.