Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Report Non-existing target/source properties as errors #177

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
import static org.mapstruct.intellij.util.MapstructUtil.isMapperConfig;

/**
* Inspection that checks if a mapping class (a class that contains at lease one mapping method) is anntoated with
* Inspection that checks if a mapping class (a class that contains at lease one mapping method) is annotated with
* {@link org.mapstruct.Mapper} or {@link org.mapstruct.MapperConfig}.
*
* @author Filip Hrisafov
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.mapstruct.intellij.MapStructBundle;
import org.mapstruct.intellij.util.MapstructAnnotationUtils;
import org.mapstruct.intellij.util.MapstructUtil;

/**
Expand All @@ -34,7 +35,7 @@ void visitMappingAnnotation( @NotNull ProblemsHolder problemsHolder, @NotNull Ps
}

private static boolean isIgnoreByDefaultEnabled(@NotNull PsiAnnotation psiAnnotation) {
PsiMethod annotatedMethod = getAnnotatedMethod( psiAnnotation );
PsiMethod annotatedMethod = MapstructAnnotationUtils.getAnnotatedMethod(psiAnnotation );
if (annotatedMethod == null) {
return false;
}
Expand All @@ -47,49 +48,4 @@ private static boolean isIgnoreByDefaultEnabled(@NotNull PsiAnnotation psiAnnota
return ignoreByDefault instanceof PsiLiteralExpression
&& Boolean.TRUE.equals( ((PsiLiteralExpression) ignoreByDefault).getValue() );
}

@Nullable
private static PsiMethod getAnnotatedMethod(@NotNull PsiAnnotation psiAnnotation) {
PsiElement psiAnnotationParent = psiAnnotation.getParent();
if (psiAnnotationParent == null) {
return null;
}
PsiElement psiAnnotationParentParent = psiAnnotationParent.getParent();
if (psiAnnotationParentParent instanceof PsiMethod) {
// directly annotated with @Mapping
return (PsiMethod) psiAnnotationParentParent;
}

PsiElement psiAnnotationParentParentParent = psiAnnotationParentParent.getParent();
if (psiAnnotationParentParentParent instanceof PsiAnnotation) {
// inside @Mappings without array
PsiElement mappingsAnnotationParent = psiAnnotationParentParentParent.getParent();
if (mappingsAnnotationParent == null) {
return null;
}
PsiElement mappingsAnnotationParentParent = mappingsAnnotationParent.getParent();
if (mappingsAnnotationParentParent instanceof PsiMethod) {
return (PsiMethod) mappingsAnnotationParentParent;
}
return null;
}
else if (psiAnnotationParentParentParent instanceof PsiAnnotationParamListImpl) {
// inside @Mappings wit array
PsiElement mappingsArray = psiAnnotationParentParentParent.getParent();
if (mappingsArray == null) {
return null;
}
PsiElement mappingsAnnotationParent = mappingsArray.getParent();
if (mappingsAnnotationParent == null) {
return null;
}
PsiElement mappingsAnnotationParentParent = mappingsAnnotationParent.getParent();
if (mappingsAnnotationParentParent instanceof PsiMethod) {
return (PsiMethod) mappingsAnnotationParentParent;
}
return null;

}
return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package org.mapstruct.intellij.inspection;

import com.intellij.codeInsight.AnnotationUtil;
import com.intellij.codeInsight.intention.QuickFixFactory;
import com.intellij.codeInspection.LocalQuickFix;
import com.intellij.codeInspection.LocalQuickFixOnPsiElement;
import com.intellij.codeInspection.ProblemHighlightType;
import com.intellij.codeInspection.ProblemsHolder;
import com.intellij.codeInspection.util.IntentionFamilyName;
import com.intellij.codeInspection.util.IntentionName;
import com.intellij.openapi.project.Project;
import com.intellij.psi.PsiAnnotation;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiMethod;
import com.intellij.psi.PsiNameValuePair;
import com.intellij.psi.PsiType;
import org.jetbrains.annotations.NotNull;
import org.mapstruct.intellij.MapStructBundle;
import org.mapstruct.intellij.util.MapStructVersion;
import org.mapstruct.intellij.util.MapstructAnnotationUtils;
import org.mapstruct.intellij.util.MapstructUtil;
import org.mapstruct.intellij.util.TargetUtils;

import java.util.Set;

import static org.mapstruct.intellij.util.TargetUtils.getTargetType;

public class NonExistingTargetPropertiesInspection extends MappingAnnotationInspectionBase {

@Override
void visitMappingAnnotation(
@NotNull final ProblemsHolder problemsHolder,
@NotNull final PsiAnnotation psiAnnotation,
@NotNull final MappingAnnotation mappingAnnotation) {

MapStructVersion version = MapstructUtil.resolveMapStructProjectVersion(problemsHolder.getFile());

PsiNameValuePair targetProperty = mappingAnnotation.getTargetProperty();
if (targetProperty != null) {

PsiMethod method = MapstructAnnotationUtils.getAnnotatedMethod(psiAnnotation);
if (method != null) {
PsiType targetType = getTargetType(method);
if (targetType != null && targetProperty.getValue() != null) {
Set<String> targets = TargetUtils.findAllTargetProperties(targetType, version, method);

String value = AnnotationUtil.getStringAttributeValue(targetProperty.getValue());

if (value != null && !targets.contains(getBaseTarget(value))) {

LocalQuickFix quickFix = QuickFixFactory.getInstance().createDeleteFix(
psiAnnotation,
MapStructBundle.message(
"intention.remove.non.existing.mapping.declaration",
value
)
);

problemsHolder.registerProblem(
targetProperty.getValue(),
MapStructBundle.message(
"inspection.non.existing.target.property",
value
),
ProblemHighlightType.ERROR,
quickFix
);
}
}
}
}
}

@NotNull
private static String getBaseTarget(@NotNull String target) {
int dotIndex = target.indexOf( "." );
if ( dotIndex > 0 ) {
return target.substring( 0, dotIndex );
}
return target;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
import com.intellij.psi.PsiNameValuePair;
import com.intellij.psi.PsiReference;
import com.intellij.psi.codeStyle.JavaCodeStyleManager;
import com.intellij.psi.impl.source.tree.java.PsiAnnotationParamListImpl;
import com.intellij.util.IncorrectOperationException;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NotNull;
Expand Down Expand Up @@ -610,4 +611,48 @@ private static ReportingPolicy getUnmappedTargetPolicyPolicyFromAnnotation(
}
}

@Nullable
public static PsiMethod getAnnotatedMethod(@NotNull PsiAnnotation psiAnnotation) {
PsiElement psiAnnotationParent = psiAnnotation.getParent();
if (psiAnnotationParent == null) {
return null;
}
PsiElement psiAnnotationParentParent = psiAnnotationParent.getParent();
if (psiAnnotationParentParent instanceof PsiMethod) {
// directly annotated with @Mapping
return (PsiMethod) psiAnnotationParentParent;
}

PsiElement psiAnnotationParentParentParent = psiAnnotationParentParent.getParent();
if (psiAnnotationParentParentParent instanceof PsiAnnotation) {
// inside @Mappings without array
PsiElement mappingsAnnotationParent = psiAnnotationParentParentParent.getParent();
if (mappingsAnnotationParent == null) {
return null;
}
PsiElement mappingsAnnotationParentParent = mappingsAnnotationParent.getParent();
if (mappingsAnnotationParentParent instanceof PsiMethod) {
return (PsiMethod) mappingsAnnotationParentParent;
}
return null;
}
else if (psiAnnotationParentParentParent instanceof PsiAnnotationParamListImpl) {
// inside @Mappings wit array
PsiElement mappingsArray = psiAnnotationParentParentParent.getParent();
if (mappingsArray == null) {
return null;
}
PsiElement mappingsAnnotationParent = mappingsArray.getParent();
if (mappingsAnnotationParent == null) {
return null;
}
PsiElement mappingsAnnotationParentParent = mappingsAnnotationParent.getParent();
if (mappingsAnnotationParentParent instanceof PsiMethod) {
return (PsiMethod) mappingsAnnotationParentParent;
}
return null;

}
return null;
}
}
9 changes: 9 additions & 0 deletions src/main/resources/META-INF/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,15 @@
shortName="UnmappedTargetProperties"
implementationClass="org.mapstruct.intellij.inspection.UnmappedTargetPropertiesInspection"/>

<localInspection
language="JAVA"
enabledByDefault="true"
level="WARNING"
bundle="org.mapstruct.intellij.messages.MapStructBundle"
key="inspection.unmapped.target.properties"
shortName="NonExistingTargetProperties"
implementationClass="org.mapstruct.intellij.inspection.NonExistingTargetPropertiesInspection"/>

<localInspection
language="JAVA"
enabledByDefault="true"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<html>
<body>
<p>
This inspection reports when a target property does not exist.
</p>
<p>
<pre><code>
//wrong
@Mapper
public interface EmployeeMapper {
@Mapping(source = "employeeName", target = "nonExistingField")
Employee toEmployee(EmployeeDto employeeDto);
}
</code></pre>
</p>
<p>
<pre><code>
//correct
@Mapper
public interface EmployeeMapper {
@Mapping(source = "employeeName", target = "name")
Employee toEmployee(EmployeeDto employeeDto);
}
</code></pre>
</p>
<!-- tooltip end -->
</body>
</html>
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ inspection.wrong.map.mapping.map.type.raw=Raw map used for mapping Map to Bean
inspection.wrong.map.mapping.map.type.raw.set.default=Replace {0} with {0}<String, String>
inspection.wrong.map.mapping.map.key=Key must be of type String for mapping Map to Bean
inspection.wrong.map.mapping.map.key.change.to.string=Change key type to String
inspection.non.existing.target.property=Non-existing target property: {0}
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
Expand All @@ -34,6 +35,7 @@ intention.not.null.checkable.property.source.used.with.default.property=Remove d
intention.java.expression.remove.unnecessary.whitespace=Remove unnecessary whitespaces
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.non.existing.mapping.declaration=Remove target ''{0}'' @Mapping declaration
plugin.settings.title=MapStruct
plugin.settings.quickFix.title=Quick fix properties
plugin.settings.quickFix.preferSourceBeforeTargetInMapping=Prefer source before target in @Mapping
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package org.mapstruct.intellij.inspection;

import com.intellij.codeInspection.LocalInspectionTool;
import org.jetbrains.annotations.NotNull;

public class NonExistingTargetPropertyInspectionTest extends BaseInspectionTest {

@Override
protected @NotNull Class<? extends LocalInspectionTool> getInspection() {
return NonExistingTargetPropertiesInspection.class;
}

public void testNonExistingProperty() {
doTest();
}
}
41 changes: 41 additions & 0 deletions testData/inspection/NonExistingProperty.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* 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.Mapping;
import org.mapstruct.Mappings;

class Source {

private String name;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}
}

class Target {

private String name;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}
}

@Mapper
interface SingleMappingMapper {
@Mapping(target = <error descr="Non-existing target property: nonExistingField">"nonExistingField"</error>, ignore = true)
Target map(Source source);
}