From 8acf4e27c5e58e6866743f2cb2981b0fee6a9ae4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20Kn=C3=B6dlseder?= Date: Wed, 14 May 2025 18:37:15 +0200 Subject: [PATCH 1/7] fix jframe (actually windows) dispose detector --- .gitignore | 4 +- .../perfume/JFrameDisposeDetector.java | 52 +++++++++---------- .../detectors/JFrameDisposeDetectorTest.java | 43 +++++++-------- .../detectors/swing/JFrameDispose.java | 8 +++ 4 files changed, 59 insertions(+), 48 deletions(-) diff --git a/.gitignore b/.gitignore index bb8f7c5..d45abf9 100644 --- a/.gitignore +++ b/.gitignore @@ -38,5 +38,7 @@ target/ # Any pictures and dot files graphics/ -# Folder I use for output of manual tests +# Folder for output of manual tests manual_test_results/ + +.DS_Store diff --git a/src/main/java/de/jsilbereisen/perfumator/engine/detector/perfume/JFrameDisposeDetector.java b/src/main/java/de/jsilbereisen/perfumator/engine/detector/perfume/JFrameDisposeDetector.java index 68266a0..53a3eec 100644 --- a/src/main/java/de/jsilbereisen/perfumator/engine/detector/perfume/JFrameDisposeDetector.java +++ b/src/main/java/de/jsilbereisen/perfumator/engine/detector/perfume/JFrameDisposeDetector.java @@ -2,8 +2,7 @@ import com.github.javaparser.ast.CompilationUnit; import com.github.javaparser.ast.expr.MethodCallExpr; -import com.github.javaparser.resolution.model.typesystem.ReferenceTypeImpl; -import com.github.javaparser.resolution.types.ResolvedType; +import com.github.javaparser.resolution.declarations.ResolvedMethodDeclaration; import com.github.javaparser.symbolsolver.javaparsermodel.JavaParserFacade; import de.jsilbereisen.perfumator.engine.detector.Detector; import de.jsilbereisen.perfumator.model.DetectedInstance; @@ -16,7 +15,7 @@ /** * {@link Detector} for the "JFrame dispose" {@link Perfume}. - * Detects the perfume only if the method is part of the {@link javax.swing.JFrame} class. + * Detects the perfume only if the method is part of the {@link java.awt.Window} class. */ public class JFrameDisposeDetector implements Detector { @@ -25,14 +24,14 @@ public class JFrameDisposeDetector implements Detector { private JavaParserFacade analysisContext; private static final String DISPOSE_METHOD_NAME = "dispose"; - private static final String QUALIFIED_JFRAME_CLASS_NAME = "javax.swing.JFrame"; + private static final String QUALIFIED_METHOD_NAME = "java.awt.Window"; @Override public @NotNull List> detect(@NotNull CompilationUnit astRoot) { List> detectedInstances = new ArrayList<>(); List disposeMethodCallExpressions = getJFrameDisposeMethodCalls(astRoot); - disposeMethodCallExpressions - .forEach(expr -> detectedInstances.add(DetectedInstance.from(expr, perfume, astRoot))); + disposeMethodCallExpressions.forEach(expr -> + detectedInstances.add(DetectedInstance.from(expr, perfume, astRoot))); return detectedInstances; } @@ -47,25 +46,26 @@ public void setAnalysisContext(@Nullable JavaParserFacade analysisContext) { } private List getJFrameDisposeMethodCalls(@NotNull CompilationUnit astRoot) { - return astRoot.findAll(MethodCallExpr.class, expr -> { - if (!expr.getNameAsString().equals(DISPOSE_METHOD_NAME)) { - return false; - } - var scope = expr.getScope(); - if (scope.isPresent()) { - ResolvedType resolvedType; - try { - resolvedType = scope.get().calculateResolvedType(); - } catch (Exception e) { - System.out.println(expr.getNameAsString()); - System.out.println(e.getMessage()); - return false; - } - if (resolvedType instanceof ReferenceTypeImpl referenceType) { - return referenceType.getQualifiedName().equals(QUALIFIED_JFRAME_CLASS_NAME); - } - } - return false; - }); + return astRoot.findAll(MethodCallExpr.class).stream() + // only consider methods with name "dispose" + .filter(expr -> expr.getNameAsString().equals(DISPOSE_METHOD_NAME)) + // ensure that the declaring class is JFrame + .filter(expr -> { + ResolvedMethodDeclaration disposeDeclaration; + try { + disposeDeclaration = analysisContext.solve(expr).getCorrespondingDeclaration(); + } catch (UnsupportedOperationException e) { + e.printStackTrace(); + return false; + } + var referenceType = disposeDeclaration.declaringType().asReferenceType(); + if (referenceType.getQualifiedName().equals(QUALIFIED_METHOD_NAME)) { + return true; + } else { + return referenceType.getAllAncestors().stream() + .anyMatch(ancestor -> + ancestor.getQualifiedName().equals(QUALIFIED_METHOD_NAME)); + } + }).toList(); } } diff --git a/src/test/java/detectors/JFrameDisposeDetectorTest.java b/src/test/java/detectors/JFrameDisposeDetectorTest.java index f6919a0..ab1aa8c 100644 --- a/src/test/java/detectors/JFrameDisposeDetectorTest.java +++ b/src/test/java/detectors/JFrameDisposeDetectorTest.java @@ -1,6 +1,7 @@ package detectors; import com.github.javaparser.ast.CompilationUnit; +import com.github.javaparser.symbolsolver.javaparsermodel.JavaParserFacade; import de.jsilbereisen.perfumator.engine.detector.Detector; import de.jsilbereisen.perfumator.engine.detector.perfume.JFrameDisposeDetector; import de.jsilbereisen.perfumator.model.CodeRange; @@ -12,6 +13,7 @@ import java.nio.file.Path; import java.util.List; +import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; @@ -24,38 +26,37 @@ public class JFrameDisposeDetectorTest extends AbstractDetectorTest { private static Detector detector; - private static CompilationUnit ast; - @BeforeAll static void init() { perfume = new Perfume(); - perfume.setName("Assert All"); + perfume.setName("JFrameDispose"); detector = new JFrameDisposeDetector(); detector.setConcreteDetectable(perfume); - - ast = parseAstForFile(TEST_FILES_DIR.resolve("JFrameDispose.java")); } @Test void detect() { + JavaParserFacade analysisContext = getAnalysisContext(parser, TEST_FILES_DIR); + CompilationUnit ast = parseAstForFile(parser, TEST_FILES_DIR.resolve("JFrameDispose.java")); + detector.setAnalysisContext(analysisContext); List> detections = detector.detect(ast); - assertThat(detections).hasSize(3); - - DetectedInstance detection = detections.get(0); - assertThat(detection.getDetectable()).isEqualTo(perfume); - assertThat(detection.getTypeName()).isEqualTo("JFrameDispose"); - assertThat(detection.getCodeRanges()).containsExactly(CodeRange.of(18, 9, 18, 24)); - - detection = detections.get(1); - assertThat(detection.getDetectable()).isEqualTo(perfume); - assertThat(detection.getTypeName()).isEqualTo("JFrameDispose"); - assertThat(detection.getCodeRanges()).containsExactly(CodeRange.of(19, 9, 19, 24)); - - detection = detections.get(2); - assertThat(detection.getDetectable()).isEqualTo(perfume); - assertThat(detection.getTypeName()).isEqualTo("JFrameDispose"); - assertThat(detection.getCodeRanges()).containsExactly(CodeRange.of(20, 9, 20, 24)); + final int NUM_DETECTIONS = 5; + assertThat(detections).hasSize(NUM_DETECTIONS); + Map codeRanges = Map.of( + 0, CodeRange.of(15, 13, 15, 27), + 1, CodeRange.of(25, 9, 25, 24), + 2, CodeRange.of(26, 9, 26, 24), + 3, CodeRange.of(27, 9, 27, 24), + 4, CodeRange.of(28, 9, 28, 38) + ); + assertThat(detections).hasSize(5); + for (int i = 0; i < NUM_DETECTIONS; i++) { + DetectedInstance detected = detections.get(i); + assertThat(detected.getDetectable()).isEqualTo(perfume); + assertThat(detected.getTypeName()).isEqualTo("JFrameDispose"); + assertThat(detected.getCodeRanges()).containsExactly(codeRanges.get(i)); + } } } diff --git a/src/test/resources/detectors/swing/JFrameDispose.java b/src/test/resources/detectors/swing/JFrameDispose.java index 78c2f43..33c1ecc 100644 --- a/src/test/resources/detectors/swing/JFrameDispose.java +++ b/src/test/resources/detectors/swing/JFrameDispose.java @@ -9,6 +9,13 @@ public void dispose() { } } + class JFrameSubClass extends JFrame { + @Override + public void dispose() { + super.dispose(); + } + } + public static void main(String[] args) { NoRealJFrame frame = new NoRealJFrame(); JFrame frame1 = new JFrame(); @@ -18,5 +25,6 @@ public static void main(String[] args) { frame1.dispose(); frame2.dispose(); frame3.dispose(); + new JFrameSubClass().dispose(); } } From b36e2f97f6894bcadac17325663405c591900b09 Mon Sep 17 00:00:00 2001 From: chrisknedl Date: Wed, 14 May 2025 19:34:03 +0200 Subject: [PATCH 2/7] improve the thread-safe swing detector --- .../perfume/JFrameDisposeDetector.java | 6 ++-- .../detector/perfume/SwingTimerDetector.java | 3 +- .../perfume/ThreadSafeSwingDetector.java | 31 ++++++------------- .../ThreadSafeSwingDetectorTest.java | 19 ++++++++---- 4 files changed, 26 insertions(+), 33 deletions(-) diff --git a/src/main/java/de/jsilbereisen/perfumator/engine/detector/perfume/JFrameDisposeDetector.java b/src/main/java/de/jsilbereisen/perfumator/engine/detector/perfume/JFrameDisposeDetector.java index 53a3eec..c153642 100644 --- a/src/main/java/de/jsilbereisen/perfumator/engine/detector/perfume/JFrameDisposeDetector.java +++ b/src/main/java/de/jsilbereisen/perfumator/engine/detector/perfume/JFrameDisposeDetector.java @@ -24,7 +24,7 @@ public class JFrameDisposeDetector implements Detector { private JavaParserFacade analysisContext; private static final String DISPOSE_METHOD_NAME = "dispose"; - private static final String QUALIFIED_METHOD_NAME = "java.awt.Window"; + private static final String DECLARING_CLASS = "java.awt.Window"; @Override public @NotNull List> detect(@NotNull CompilationUnit astRoot) { @@ -59,12 +59,12 @@ private List getJFrameDisposeMethodCalls(@NotNull CompilationUni return false; } var referenceType = disposeDeclaration.declaringType().asReferenceType(); - if (referenceType.getQualifiedName().equals(QUALIFIED_METHOD_NAME)) { + if (referenceType.getQualifiedName().equals(DECLARING_CLASS)) { return true; } else { return referenceType.getAllAncestors().stream() .anyMatch(ancestor -> - ancestor.getQualifiedName().equals(QUALIFIED_METHOD_NAME)); + ancestor.getQualifiedName().equals(DECLARING_CLASS)); } }).toList(); } diff --git a/src/main/java/de/jsilbereisen/perfumator/engine/detector/perfume/SwingTimerDetector.java b/src/main/java/de/jsilbereisen/perfumator/engine/detector/perfume/SwingTimerDetector.java index 9e95b8b..a6483cb 100644 --- a/src/main/java/de/jsilbereisen/perfumator/engine/detector/perfume/SwingTimerDetector.java +++ b/src/main/java/de/jsilbereisen/perfumator/engine/detector/perfume/SwingTimerDetector.java @@ -57,8 +57,7 @@ private List getNewTimerExpressions(@NotNull CompilationUnit try { resolvedType = expr.calculateResolvedType(); } catch (Exception e) { - System.out.println(expr); - System.out.println(e.getMessage()); + e.printStackTrace(); return false; } return resolvedType instanceof ReferenceTypeImpl referenceType diff --git a/src/main/java/de/jsilbereisen/perfumator/engine/detector/perfume/ThreadSafeSwingDetector.java b/src/main/java/de/jsilbereisen/perfumator/engine/detector/perfume/ThreadSafeSwingDetector.java index c2b7019..d5b34dc 100644 --- a/src/main/java/de/jsilbereisen/perfumator/engine/detector/perfume/ThreadSafeSwingDetector.java +++ b/src/main/java/de/jsilbereisen/perfumator/engine/detector/perfume/ThreadSafeSwingDetector.java @@ -3,8 +3,6 @@ import com.github.javaparser.ast.CompilationUnit; import com.github.javaparser.ast.expr.MethodCallExpr; import com.github.javaparser.resolution.declarations.ResolvedMethodDeclaration; -import com.github.javaparser.resolution.model.typesystem.ReferenceTypeImpl; -import com.github.javaparser.resolution.types.ResolvedType; import com.github.javaparser.symbolsolver.javaparsermodel.JavaParserFacade; import de.jsilbereisen.perfumator.engine.detector.Detector; import de.jsilbereisen.perfumator.model.DetectedInstance; @@ -29,9 +27,7 @@ public class ThreadSafeSwingDetector implements Detector { private final static String INVOKE_LATER = "invokeLater"; private final static String INVOKE_AND_WAIT = "invokeAndWait"; - private final static Set QUALIFIED_METHOD_NAMES - = Set.of("javax.swing.SwingUtilities.invokeLater", "javax.swing.SwingUtilities.invokeAndWait"); - private final static String IMPORT = "javax.swing.SwingUtilities"; + private final static String DECLARING_CLASS = "javax.swing.SwingUtilities"; @Override public @NotNull List> detect(@NotNull CompilationUnit astRoot) { @@ -57,24 +53,15 @@ private List getInvokeLaterInvokeAndWaitMethodCalls(@NotNull Com if (!expr.getNameAsString().contains(INVOKE_LATER) && !expr.getNameAsString().contains(INVOKE_AND_WAIT)) { return false; } - if (expr.getScope().isPresent()) { - // for non-static imports - ResolvedType resolvedType; - try { - resolvedType = expr.getScope().get().calculateResolvedType(); - } catch (Exception e) { - System.out.println(expr.getNameAsString()); - System.out.println(e.getMessage()); - return false; - } - return resolvedType instanceof ReferenceTypeImpl referenceType - && referenceType.getQualifiedName().equals(IMPORT); - } else { - // for static imports - ResolvedMethodDeclaration resolvedMethodDeclaration = expr.resolve(); - String qualifiedName = resolvedMethodDeclaration.getQualifiedName(); - return QUALIFIED_METHOD_NAMES.contains(qualifiedName); + ResolvedMethodDeclaration methodDeclaration; + try { + methodDeclaration = analysisContext.solve(expr).getCorrespondingDeclaration(); + } catch (UnsupportedOperationException e) { + e.printStackTrace(); + return false; } + var referenceType = methodDeclaration.declaringType().asReferenceType(); + return DECLARING_CLASS.contains(referenceType.getQualifiedName()); }); } } diff --git a/src/test/java/detectors/ThreadSafeSwingDetectorTest.java b/src/test/java/detectors/ThreadSafeSwingDetectorTest.java index 0c78ae3..1e3345d 100644 --- a/src/test/java/detectors/ThreadSafeSwingDetectorTest.java +++ b/src/test/java/detectors/ThreadSafeSwingDetectorTest.java @@ -1,6 +1,7 @@ package detectors; import com.github.javaparser.ast.CompilationUnit; +import com.github.javaparser.symbolsolver.javaparsermodel.JavaParserFacade; import de.jsilbereisen.perfumator.engine.detector.Detector; import de.jsilbereisen.perfumator.engine.detector.perfume.ThreadSafeSwingDetector; import de.jsilbereisen.perfumator.model.CodeRange; @@ -24,8 +25,6 @@ public class ThreadSafeSwingDetectorTest extends AbstractDetectorTest { private static Detector detector; - private static CompilationUnit ast; - @BeforeAll static void init() { perfume = new Perfume(); @@ -37,7 +36,9 @@ static void init() { @Test void detectStaticImport() { - ast = parseAstForFile(TEST_FILES_DIR.resolve("InvokeLaterInvokeAndWaitStaticImport.java")); + JavaParserFacade analysisContext = getAnalysisContext(parser, TEST_FILES_DIR); + CompilationUnit ast = parseAstForFile(TEST_FILES_DIR.resolve("InvokeLaterInvokeAndWaitStaticImport.java")); + detector.setAnalysisContext(analysisContext); List> detections = detector.detect(ast); assertThat(detections).hasSize(2); @@ -55,7 +56,9 @@ void detectStaticImport() { @Test void detectWithoutStaticImport() { - ast = parseAstForFile(TEST_FILES_DIR.resolve("InvokeLaterInvokeAndWaitNoStaticImport.java")); + JavaParserFacade analysisContext = getAnalysisContext(parser, TEST_FILES_DIR); + CompilationUnit ast = parseAstForFile(TEST_FILES_DIR.resolve("InvokeLaterInvokeAndWaitNoStaticImport.java")); + detector.setAnalysisContext(analysisContext); List> detections = detector.detect(ast); assertThat(detections).hasSize(2); @@ -73,7 +76,9 @@ void detectWithoutStaticImport() { @Test void detectWithWildcardImport() { - ast = parseAstForFile(TEST_FILES_DIR.resolve("InvokeLaterInvokeAndWaitStaticWildcardImport.java")); + JavaParserFacade analysisContext = getAnalysisContext(parser, TEST_FILES_DIR); + CompilationUnit ast = parseAstForFile(TEST_FILES_DIR.resolve("InvokeLaterInvokeAndWaitStaticWildcardImport.java")); + detector.setAnalysisContext(analysisContext); List> detections = detector.detect(ast); assertThat(detections).hasSize(2); @@ -91,7 +96,9 @@ void detectWithWildcardImport() { @Test void detectNoPerfume() { - ast = parseAstForFile(TEST_FILES_DIR.resolve("InvokeLaterInvokeAndWaitNoPerfume.java")); + JavaParserFacade analysisContext = getAnalysisContext(parser, TEST_FILES_DIR); + CompilationUnit ast = parseAstForFile(TEST_FILES_DIR.resolve("InvokeLaterInvokeAndWaitNoPerfume.java")); + detector.setAnalysisContext(analysisContext); List> detections = detector.detect(ast); assertThat(detections).isEmpty(); From 1fd8705ff250a33e28212e88c768e5611e2c1966 Mon Sep 17 00:00:00 2001 From: chrisknedl Date: Thu, 15 May 2025 13:44:10 +0200 Subject: [PATCH 3/7] improve assertAll perfume detection --- .../detector/perfume/AssertAllDetector.java | 46 +++++++------------ .../java/detectors/AssertAllDetectorTest.java | 27 ++++++++--- src/test/java/test/AbstractDetectorTest.java | 2 +- 3 files changed, 39 insertions(+), 36 deletions(-) diff --git a/src/main/java/de/jsilbereisen/perfumator/engine/detector/perfume/AssertAllDetector.java b/src/main/java/de/jsilbereisen/perfumator/engine/detector/perfume/AssertAllDetector.java index 1b54b38..f96897d 100644 --- a/src/main/java/de/jsilbereisen/perfumator/engine/detector/perfume/AssertAllDetector.java +++ b/src/main/java/de/jsilbereisen/perfumator/engine/detector/perfume/AssertAllDetector.java @@ -3,6 +3,7 @@ import com.github.javaparser.ast.CompilationUnit; import com.github.javaparser.ast.expr.MethodCallExpr; import com.github.javaparser.resolution.declarations.ResolvedMethodDeclaration; +import com.github.javaparser.resolution.declarations.ResolvedReferenceTypeDeclaration; import com.github.javaparser.resolution.model.typesystem.ReferenceTypeImpl; import com.github.javaparser.resolution.types.ResolvedType; import com.github.javaparser.symbolsolver.javaparsermodel.JavaParserFacade; @@ -29,16 +30,15 @@ public class AssertAllDetector implements Detector { private JavaParserFacade analysisContext; - private static final String QUALIFIED_ASSERT_ALL_METHOD_NAME = "org.junit.jupiter.api.Assertions.assertAll"; - private static final String ASSERTIONS_IMPORT_NAME = "org.junit.jupiter.api.Assertions"; + private static final String DECLARING_CLASS = "org.junit.jupiter.api.Assertions"; private static final String ASSERT_ALL = "assertAll"; @Override public @NotNull List> detect(@NotNull CompilationUnit astRoot) { List> detectedInstances = new ArrayList<>(); List assertAllMethodCallExpressions = getAssertAllMethodCalls(astRoot); - assertAllMethodCallExpressions - .forEach(callExpr -> detectedInstances.add(DetectedInstance.from(callExpr, perfume, astRoot))); + assertAllMethodCallExpressions.forEach(callExpr -> + detectedInstances.add(DetectedInstance.from(callExpr, perfume, astRoot))); return detectedInstances; } @@ -53,30 +53,18 @@ public void setAnalysisContext(@Nullable JavaParserFacade analysisContext) { } private List getAssertAllMethodCalls(@NotNull CompilationUnit astRoot) { - return astRoot.findAll(MethodCallExpr.class, expr -> { - // contains instead of equals because of possible 'Assertions.assertAll' calls - if (!expr.getNameAsString().contains(ASSERT_ALL)) { - return false; - } - if (expr.getScope().isPresent()) { - // for non-static imports - ResolvedType resolvedType; - try { - resolvedType = expr.getScope().get().calculateResolvedType(); - } catch (Exception e) { - System.out.println(expr.getNameAsString()); - System.out.println(e.getMessage()); - return false; - } - return resolvedType instanceof ReferenceTypeImpl referenceType - && referenceType.getQualifiedName().equals(ASSERTIONS_IMPORT_NAME); - } else { - // for static imports - Optional resolvedMethodDeclaration - = NodeUtil.resolveSafely(expr, this, expr.getNameAsString()); - return resolvedMethodDeclaration.map(methodDeclaration -> methodDeclaration.getQualifiedName().equals(QUALIFIED_ASSERT_ALL_METHOD_NAME)) - .orElse(false); - } - }); + return astRoot.findAll(MethodCallExpr.class).stream() + .filter(expr -> expr.getNameAsString().equals(ASSERT_ALL)) + .filter(expr -> { + ResolvedMethodDeclaration methodDeclaration; + try { + methodDeclaration = analysisContext.solve(expr).getCorrespondingDeclaration(); + } catch (UnsupportedOperationException e) { + e.printStackTrace(); + return false; + } + ResolvedReferenceTypeDeclaration referenceType = methodDeclaration.declaringType().asReferenceType(); + return DECLARING_CLASS.equals(referenceType.getQualifiedName()); + }).toList(); } } diff --git a/src/test/java/detectors/AssertAllDetectorTest.java b/src/test/java/detectors/AssertAllDetectorTest.java index 9342afc..0080d35 100644 --- a/src/test/java/detectors/AssertAllDetectorTest.java +++ b/src/test/java/detectors/AssertAllDetectorTest.java @@ -1,6 +1,7 @@ package detectors; import com.github.javaparser.ast.CompilationUnit; +import com.github.javaparser.symbolsolver.javaparsermodel.JavaParserFacade; import de.jsilbereisen.perfumator.engine.detector.Detector; import de.jsilbereisen.perfumator.engine.detector.perfume.AssertAllDetector; import de.jsilbereisen.perfumator.model.CodeRange; @@ -23,8 +24,6 @@ class AssertAllDetectorTest extends AbstractDetectorTest { private static Detector detector; - private static CompilationUnit ast; - @BeforeAll static void init() { perfume = new Perfume(); @@ -36,7 +35,11 @@ static void init() { @Test void detectStaticImport() { - ast = parseAstForFile(TEST_FILES_DIR.resolve("AssertAllStaticImport.java")); + JavaParserFacade analysisContext = getAnalysisContext(parser, TEST_FILES_DIR); + detector.setAnalysisContext(analysisContext); + + CompilationUnit ast = parseAstForFile(parser, TEST_FILES_DIR.resolve("AssertAllStaticImport.java")); + List> detections = detector.detect(ast); assertThat(detections).hasSize(1); @@ -50,7 +53,11 @@ void detectStaticImport() { @Test void detectWithoutStaticImport() { - ast = parseAstForFile(TEST_FILES_DIR.resolve("AssertAllNoStaticImport.java")); + JavaParserFacade analysisContext = getAnalysisContext(parser, TEST_FILES_DIR); + detector.setAnalysisContext(analysisContext); + + CompilationUnit ast = parseAstForFile(parser, TEST_FILES_DIR.resolve("AssertAllNoStaticImport.java")); + List> detections = detector.detect(ast); assertThat(detections).hasSize(1); @@ -64,7 +71,11 @@ void detectWithoutStaticImport() { @Test void detectWithWildcardImport() { - ast = parseAstForFile(TEST_FILES_DIR.resolve("AssertionsStaticWildcardImport.java")); + JavaParserFacade analysisContext = getAnalysisContext(parser, TEST_FILES_DIR); + detector.setAnalysisContext(analysisContext); + + CompilationUnit ast = parseAstForFile(parser, TEST_FILES_DIR.resolve("AssertionsStaticWildcardImport.java")); + List> detections = detector.detect(ast); assertThat(detections).hasSize(1); @@ -78,7 +89,11 @@ void detectWithWildcardImport() { @Test void detectNoPerfumeForDifferentAssertAllMethod() { - ast = parseAstForFile(TEST_FILES_DIR.resolve("AssertAllNoPerfume.java")); + JavaParserFacade analysisContext = getAnalysisContext(parser, TEST_FILES_DIR); + detector.setAnalysisContext(analysisContext); + + CompilationUnit ast = parseAstForFile(parser, TEST_FILES_DIR.resolve("AssertAllNoPerfume.java")); + List> detections = detector.detect(ast); assertThat(detections).isEmpty(); diff --git a/src/test/java/test/AbstractDetectorTest.java b/src/test/java/test/AbstractDetectorTest.java index 9f793ac..d75b324 100644 --- a/src/test/java/test/AbstractDetectorTest.java +++ b/src/test/java/test/AbstractDetectorTest.java @@ -175,7 +175,7 @@ protected static void saveAstAsDot(@NotNull Node node, @NotNull Path saveDirecto @NotNull protected static JavaParserFacade getAnalysisContext(@NotNull JavaParser parser, @NotNull Path... dependencies) { // Very very ugly, but i don't see any other way to get the created TypeSolver at the moment - CombinedTypeSolver typeSolver = new CombinedTypeSolver(new ReflectionTypeSolver()); + CombinedTypeSolver typeSolver = new CombinedTypeSolver(new ReflectionTypeSolver(false)); /* try { typeSolver = (CombinedTypeSolver) FieldUtils.readField(strategy, "typeSolver", true); From a781fc9465de95c4d87570783a703799b70c22ce Mon Sep 17 00:00:00 2001 From: chrisknedl Date: Thu, 15 May 2025 15:01:17 +0200 Subject: [PATCH 4/7] improve paramterized test detector --- .../perfume/ParameterizedTestDetector.java | 33 ++++++++++++------- .../ParameterizedTestDetectorTest.java | 15 +++++---- 2 files changed, 30 insertions(+), 18 deletions(-) diff --git a/src/main/java/de/jsilbereisen/perfumator/engine/detector/perfume/ParameterizedTestDetector.java b/src/main/java/de/jsilbereisen/perfumator/engine/detector/perfume/ParameterizedTestDetector.java index 2c4723b..96ce820 100644 --- a/src/main/java/de/jsilbereisen/perfumator/engine/detector/perfume/ParameterizedTestDetector.java +++ b/src/main/java/de/jsilbereisen/perfumator/engine/detector/perfume/ParameterizedTestDetector.java @@ -2,6 +2,8 @@ import com.github.javaparser.ast.CompilationUnit; import com.github.javaparser.ast.body.MethodDeclaration; +import com.github.javaparser.resolution.declarations.ResolvedAnnotationDeclaration; +import com.github.javaparser.resolution.declarations.ResolvedReferenceTypeDeclaration; import com.github.javaparser.symbolsolver.javaparsermodel.JavaParserFacade; import de.jsilbereisen.perfumator.engine.detector.Detector; import de.jsilbereisen.perfumator.model.DetectedInstance; @@ -23,16 +25,16 @@ public class ParameterizedTestDetector implements Detector { private Perfume perfume; private JavaParserFacade analysisContext; - - private static final String PARAMETERIZED_TEST_PACKAGE = "org.junit.jupiter.params."; + + private static final String QUALIFIED_NAME = "org.junit.jupiter.params.ParameterizedTest"; private static final String PARAMETERIZED_TEST_IDENTIFIER = "ParameterizedTest"; @Override public @NotNull List> detect(@NotNull CompilationUnit astRoot) { List> detectedInstances = new ArrayList<>(); List parameterizedTestMethodDeclarations = getParameterizedTestMethodDeclarations(astRoot); - parameterizedTestMethodDeclarations - .forEach(declaration -> detectedInstances.add(DetectedInstance.from(declaration, perfume, astRoot))); + parameterizedTestMethodDeclarations.forEach(declaration + -> detectedInstances.add(DetectedInstance.from(declaration, perfume, astRoot))); return detectedInstances; } @@ -47,13 +49,20 @@ public void setAnalysisContext(@Nullable JavaParserFacade analysisContext) { } private List getParameterizedTestMethodDeclarations(@NotNull CompilationUnit astRoot) { - return astRoot.findAll(MethodDeclaration.class, methodDeclaration -> methodDeclaration.getAnnotations().stream() - // filter out annotations that do not contain 'ParameterizedTest' - .filter(annotation -> annotation.getNameAsString().contains(PARAMETERIZED_TEST_IDENTIFIER)) - // try to resolve the symbol in order to get the declaration - .map(paramTestAnnotation -> NodeUtil.resolveSafely(paramTestAnnotation, this, paramTestAnnotation.getNameAsString())) - .filter(Optional::isPresent) - .map(resolvedAnnotationDeclaration -> resolvedAnnotationDeclaration.get().getQualifiedName()) - .anyMatch(qualifiedName -> qualifiedName.equals(PARAMETERIZED_TEST_PACKAGE + PARAMETERIZED_TEST_IDENTIFIER))); + return astRoot.findAll(MethodDeclaration.class, expr -> { + var annotations = expr.getAnnotations(); + annotations.removeIf(annotation -> + !annotation.getNameAsString().contains(PARAMETERIZED_TEST_IDENTIFIER)); + return annotations.stream().anyMatch(annotation -> { + ResolvedAnnotationDeclaration resolvedAnnotationDeclaration; + try { + resolvedAnnotationDeclaration = analysisContext.solve(annotation).getCorrespondingDeclaration(); + } catch (UnsupportedOperationException e) { + e.printStackTrace(); + return false; + } + return QUALIFIED_NAME.equals(resolvedAnnotationDeclaration.getQualifiedName()); + }); + }); } } diff --git a/src/test/java/detectors/ParameterizedTestDetectorTest.java b/src/test/java/detectors/ParameterizedTestDetectorTest.java index 97dc12e..c9862ae 100644 --- a/src/test/java/detectors/ParameterizedTestDetectorTest.java +++ b/src/test/java/detectors/ParameterizedTestDetectorTest.java @@ -1,6 +1,7 @@ package detectors; import com.github.javaparser.ast.CompilationUnit; +import com.github.javaparser.symbolsolver.javaparsermodel.JavaParserFacade; import de.jsilbereisen.perfumator.engine.detector.Detector; import de.jsilbereisen.perfumator.engine.detector.perfume.ParameterizedTestDetector; import de.jsilbereisen.perfumator.model.CodeRange; @@ -23,8 +24,6 @@ public class ParameterizedTestDetectorTest extends AbstractDetectorTest { private static Detector detector; - private static CompilationUnit ast; - @BeforeAll static void init() { perfume = new Perfume(); @@ -34,9 +33,11 @@ static void init() { detector.setConcreteDetectable(perfume); } - @Test + @Test() void detect() { - ast = parseAstForFile(TEST_FILES_DIR.resolve("ParameterizedTests.java")); + JavaParserFacade analysisContext = getAnalysisContext(parser, TEST_FILES_DIR); + detector.setAnalysisContext(analysisContext); + CompilationUnit ast = parseAstForFile(parser, TEST_FILES_DIR.resolve("ParameterizedTests.java")); List> detections = detector.detect(ast); assertThat(detections).hasSize(1); @@ -50,11 +51,13 @@ void detect() { @Test void detectNoPerfumeForOnwAnnotation() { - ast = parseAstForFile(TEST_FILES_DIR.resolve("ParameterizedTestsOwnAnnotation.java")); + JavaParserFacade analysisContext = getAnalysisContext(parser, TEST_FILES_DIR); + detector.setAnalysisContext(analysisContext); + CompilationUnit ast = parseAstForFile(TEST_FILES_DIR.resolve("ParameterizedTestsOwnAnnotation.java")); List> detections = detector.detect(ast); assertThat(detections).hasSize(1); - + DetectedInstance detection = detections.get(0); assertThat(detection.getDetectable()).isEqualTo(perfume); assertThat(detection.getTypeName()).isEqualTo("ParameterizedTestsOwnAnnotation"); From bd7826cafd3541f414d8d49aec9b8cf970d41280 Mon Sep 17 00:00:00 2001 From: chrisknedl Date: Thu, 15 May 2025 15:27:57 +0200 Subject: [PATCH 5/7] improve setup and teardown method detector --- .../SetupAndTeardownMethodDetector.java | 28 +++++++++++++------ .../SetupAndTeardownMethodDetectorTest.java | 11 +++++--- 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/src/main/java/de/jsilbereisen/perfumator/engine/detector/perfume/SetupAndTeardownMethodDetector.java b/src/main/java/de/jsilbereisen/perfumator/engine/detector/perfume/SetupAndTeardownMethodDetector.java index 6858487..19baa50 100644 --- a/src/main/java/de/jsilbereisen/perfumator/engine/detector/perfume/SetupAndTeardownMethodDetector.java +++ b/src/main/java/de/jsilbereisen/perfumator/engine/detector/perfume/SetupAndTeardownMethodDetector.java @@ -2,6 +2,7 @@ import com.github.javaparser.ast.CompilationUnit; import com.github.javaparser.ast.body.MethodDeclaration; +import com.github.javaparser.resolution.declarations.ResolvedAnnotationDeclaration; import com.github.javaparser.symbolsolver.javaparsermodel.JavaParserFacade; import de.jsilbereisen.perfumator.engine.detector.Detector; import de.jsilbereisen.perfumator.model.DetectedInstance; @@ -56,13 +57,24 @@ private Set getQualifiedAnnotations() { } private List getSetupAndTeardownMethodDeclarations(@NotNull CompilationUnit astRoot) { - return astRoot.findAll(MethodDeclaration.class, methodDeclaration -> methodDeclaration.getAnnotations().stream() - // filter out annotations that do not contain any of the four relevant annotations - .filter(annotation -> TEST_ANNOTATIONS.stream().anyMatch(testAnnotation -> annotation.getNameAsString().contains(testAnnotation))) - // try to resolve the symbol in order to get the declaration - .map(testAnnotation -> NodeUtil.resolveSafely(testAnnotation, this, testAnnotation.getNameAsString())) - .filter(Optional::isPresent) - .map(resolvedAnnotationDeclaration -> resolvedAnnotationDeclaration.get().getQualifiedName()) - .anyMatch(qualifiedName -> getQualifiedAnnotations().contains(qualifiedName))); + return astRoot.findAll(MethodDeclaration.class, expr -> { + var annotations = expr.getAnnotations(); + // remove annotation if it does not contain any of the four annotations in TEST_ANNOTATIONS + annotations.removeIf(annotation -> + TEST_ANNOTATIONS.stream().noneMatch(allowedAnnotation -> annotation.getNameAsString().contains(allowedAnnotation))); + return annotations.stream().anyMatch(annotation -> { + ResolvedAnnotationDeclaration resolvedAnnotationDeclaration; + try { + resolvedAnnotationDeclaration = analysisContext.solve(annotation).getCorrespondingDeclaration(); + } catch (UnsupportedOperationException e) { + e.printStackTrace(); + return false; + } + Set fullyQualifiedAnnotations = TEST_ANNOTATIONS.stream().map(allowedAnnotation -> + IMPORT_QUALIFIER + allowedAnnotation).collect(Collectors.toSet()); + return fullyQualifiedAnnotations.stream().anyMatch(qualifiedAnnotation -> + resolvedAnnotationDeclaration.getQualifiedName().equals(qualifiedAnnotation)); + }); + }); } } diff --git a/src/test/java/detectors/SetupAndTeardownMethodDetectorTest.java b/src/test/java/detectors/SetupAndTeardownMethodDetectorTest.java index 5974370..2348cab 100644 --- a/src/test/java/detectors/SetupAndTeardownMethodDetectorTest.java +++ b/src/test/java/detectors/SetupAndTeardownMethodDetectorTest.java @@ -1,6 +1,7 @@ package detectors; import com.github.javaparser.ast.CompilationUnit; +import com.github.javaparser.symbolsolver.javaparsermodel.JavaParserFacade; import de.jsilbereisen.perfumator.engine.detector.Detector; import de.jsilbereisen.perfumator.engine.detector.perfume.SetupAndTeardownMethodDetector; import de.jsilbereisen.perfumator.model.CodeRange; @@ -23,8 +24,6 @@ public class SetupAndTeardownMethodDetectorTest extends AbstractDetectorTest { private static Detector detector; - private static CompilationUnit ast; - @BeforeAll static void init() { perfume = new Perfume(); @@ -36,7 +35,9 @@ static void init() { @Test void detect() { - ast = parseAstForFile(TEST_FILES_DIR.resolve("SetupAndTeardownMethods.java")); + JavaParserFacade analysisContext = getAnalysisContext(parser, TEST_FILES_DIR); + detector.setAnalysisContext(analysisContext); + CompilationUnit ast = parseAstForFile(TEST_FILES_DIR.resolve("SetupAndTeardownMethods.java")); List> detections = detector.detect(ast); assertThat(detections).hasSize(4); @@ -64,7 +65,9 @@ void detect() { @Test void detectWithOwnAnnotations() { - ast = parseAstForFile(TEST_FILES_DIR.resolve("SetupAndTeardownMethodsOwnAnnotations.java")); + JavaParserFacade analysisContext = getAnalysisContext(parser, TEST_FILES_DIR); + detector.setAnalysisContext(analysisContext); + CompilationUnit ast = parseAstForFile(TEST_FILES_DIR.resolve("SetupAndTeardownMethodsOwnAnnotations.java")); List> detections = detector.detect(ast); assertThat(detections).hasSize(4); From c4491ff0e23e7adf10f0229bc92157eff680e309 Mon Sep 17 00:00:00 2001 From: chrisknedl Date: Thu, 15 May 2025 15:29:41 +0200 Subject: [PATCH 6/7] cleanup --- .../detector/perfume/AssertAllDetector.java | 3 -- .../perfume/ParameterizedTestDetector.java | 2 -- .../SetupAndTeardownMethodDetector.java | 1 - .../detector/perfume/SwingTimerDetector.java | 2 +- .../perfume/ThreadSafeSwingDetector.java | 29 +++++++++---------- .../ThreadSafeSwingDetectorTest.java | 8 ++--- 6 files changed, 19 insertions(+), 26 deletions(-) diff --git a/src/main/java/de/jsilbereisen/perfumator/engine/detector/perfume/AssertAllDetector.java b/src/main/java/de/jsilbereisen/perfumator/engine/detector/perfume/AssertAllDetector.java index f96897d..a14b121 100644 --- a/src/main/java/de/jsilbereisen/perfumator/engine/detector/perfume/AssertAllDetector.java +++ b/src/main/java/de/jsilbereisen/perfumator/engine/detector/perfume/AssertAllDetector.java @@ -4,13 +4,10 @@ import com.github.javaparser.ast.expr.MethodCallExpr; import com.github.javaparser.resolution.declarations.ResolvedMethodDeclaration; import com.github.javaparser.resolution.declarations.ResolvedReferenceTypeDeclaration; -import com.github.javaparser.resolution.model.typesystem.ReferenceTypeImpl; -import com.github.javaparser.resolution.types.ResolvedType; import com.github.javaparser.symbolsolver.javaparsermodel.JavaParserFacade; import de.jsilbereisen.perfumator.engine.detector.Detector; import de.jsilbereisen.perfumator.model.DetectedInstance; import de.jsilbereisen.perfumator.model.perfume.Perfume; -import de.jsilbereisen.perfumator.util.NodeUtil; import lombok.EqualsAndHashCode; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/src/main/java/de/jsilbereisen/perfumator/engine/detector/perfume/ParameterizedTestDetector.java b/src/main/java/de/jsilbereisen/perfumator/engine/detector/perfume/ParameterizedTestDetector.java index 96ce820..403f9c3 100644 --- a/src/main/java/de/jsilbereisen/perfumator/engine/detector/perfume/ParameterizedTestDetector.java +++ b/src/main/java/de/jsilbereisen/perfumator/engine/detector/perfume/ParameterizedTestDetector.java @@ -3,12 +3,10 @@ import com.github.javaparser.ast.CompilationUnit; import com.github.javaparser.ast.body.MethodDeclaration; import com.github.javaparser.resolution.declarations.ResolvedAnnotationDeclaration; -import com.github.javaparser.resolution.declarations.ResolvedReferenceTypeDeclaration; import com.github.javaparser.symbolsolver.javaparsermodel.JavaParserFacade; import de.jsilbereisen.perfumator.engine.detector.Detector; import de.jsilbereisen.perfumator.model.DetectedInstance; import de.jsilbereisen.perfumator.model.perfume.Perfume; -import de.jsilbereisen.perfumator.util.NodeUtil; import lombok.EqualsAndHashCode; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/src/main/java/de/jsilbereisen/perfumator/engine/detector/perfume/SetupAndTeardownMethodDetector.java b/src/main/java/de/jsilbereisen/perfumator/engine/detector/perfume/SetupAndTeardownMethodDetector.java index 19baa50..b85aea3 100644 --- a/src/main/java/de/jsilbereisen/perfumator/engine/detector/perfume/SetupAndTeardownMethodDetector.java +++ b/src/main/java/de/jsilbereisen/perfumator/engine/detector/perfume/SetupAndTeardownMethodDetector.java @@ -7,7 +7,6 @@ import de.jsilbereisen.perfumator.engine.detector.Detector; import de.jsilbereisen.perfumator.model.DetectedInstance; import de.jsilbereisen.perfumator.model.perfume.Perfume; -import de.jsilbereisen.perfumator.util.NodeUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/src/main/java/de/jsilbereisen/perfumator/engine/detector/perfume/SwingTimerDetector.java b/src/main/java/de/jsilbereisen/perfumator/engine/detector/perfume/SwingTimerDetector.java index a6483cb..889186b 100644 --- a/src/main/java/de/jsilbereisen/perfumator/engine/detector/perfume/SwingTimerDetector.java +++ b/src/main/java/de/jsilbereisen/perfumator/engine/detector/perfume/SwingTimerDetector.java @@ -61,7 +61,7 @@ private List getNewTimerExpressions(@NotNull CompilationUnit return false; } return resolvedType instanceof ReferenceTypeImpl referenceType - && referenceType.getQualifiedName().equals(QUALIFIED_TIMER_NAME); + && QUALIFIED_TIMER_NAME.equals(referenceType.getQualifiedName()); }); } } diff --git a/src/main/java/de/jsilbereisen/perfumator/engine/detector/perfume/ThreadSafeSwingDetector.java b/src/main/java/de/jsilbereisen/perfumator/engine/detector/perfume/ThreadSafeSwingDetector.java index d5b34dc..80dcdac 100644 --- a/src/main/java/de/jsilbereisen/perfumator/engine/detector/perfume/ThreadSafeSwingDetector.java +++ b/src/main/java/de/jsilbereisen/perfumator/engine/detector/perfume/ThreadSafeSwingDetector.java @@ -3,6 +3,7 @@ import com.github.javaparser.ast.CompilationUnit; import com.github.javaparser.ast.expr.MethodCallExpr; import com.github.javaparser.resolution.declarations.ResolvedMethodDeclaration; +import com.github.javaparser.resolution.declarations.ResolvedReferenceTypeDeclaration; import com.github.javaparser.symbolsolver.javaparsermodel.JavaParserFacade; import de.jsilbereisen.perfumator.engine.detector.Detector; import de.jsilbereisen.perfumator.model.DetectedInstance; @@ -48,20 +49,18 @@ public void setAnalysisContext(@Nullable JavaParserFacade analysisContext) { } private List getInvokeLaterInvokeAndWaitMethodCalls(@NotNull CompilationUnit astRoot) { - return astRoot.findAll(MethodCallExpr.class, expr -> { - // contains instead of equals because of possible 'SwingUtilities.invokeLater' and '-.invokeAndWait' calls - if (!expr.getNameAsString().contains(INVOKE_LATER) && !expr.getNameAsString().contains(INVOKE_AND_WAIT)) { - return false; - } - ResolvedMethodDeclaration methodDeclaration; - try { - methodDeclaration = analysisContext.solve(expr).getCorrespondingDeclaration(); - } catch (UnsupportedOperationException e) { - e.printStackTrace(); - return false; - } - var referenceType = methodDeclaration.declaringType().asReferenceType(); - return DECLARING_CLASS.contains(referenceType.getQualifiedName()); - }); + return astRoot.findAll(MethodCallExpr.class).stream() + .filter(expr -> Set.of(INVOKE_AND_WAIT, INVOKE_LATER).contains(expr.getNameAsString())) + .filter(expr -> { + ResolvedMethodDeclaration methodDeclaration; + try { + methodDeclaration = analysisContext.solve(expr).getCorrespondingDeclaration(); + } catch (UnsupportedOperationException e) { + e.printStackTrace(); + return false; + } + ResolvedReferenceTypeDeclaration referenceType = methodDeclaration.declaringType().asReferenceType(); + return DECLARING_CLASS.equals(referenceType.getQualifiedName()); + }).toList(); } } diff --git a/src/test/java/detectors/ThreadSafeSwingDetectorTest.java b/src/test/java/detectors/ThreadSafeSwingDetectorTest.java index 1e3345d..0ffc4c2 100644 --- a/src/test/java/detectors/ThreadSafeSwingDetectorTest.java +++ b/src/test/java/detectors/ThreadSafeSwingDetectorTest.java @@ -37,8 +37,8 @@ static void init() { @Test void detectStaticImport() { JavaParserFacade analysisContext = getAnalysisContext(parser, TEST_FILES_DIR); - CompilationUnit ast = parseAstForFile(TEST_FILES_DIR.resolve("InvokeLaterInvokeAndWaitStaticImport.java")); detector.setAnalysisContext(analysisContext); + CompilationUnit ast = parseAstForFile(TEST_FILES_DIR.resolve("InvokeLaterInvokeAndWaitStaticImport.java")); List> detections = detector.detect(ast); assertThat(detections).hasSize(2); @@ -57,8 +57,8 @@ void detectStaticImport() { @Test void detectWithoutStaticImport() { JavaParserFacade analysisContext = getAnalysisContext(parser, TEST_FILES_DIR); - CompilationUnit ast = parseAstForFile(TEST_FILES_DIR.resolve("InvokeLaterInvokeAndWaitNoStaticImport.java")); detector.setAnalysisContext(analysisContext); + CompilationUnit ast = parseAstForFile(TEST_FILES_DIR.resolve("InvokeLaterInvokeAndWaitNoStaticImport.java")); List> detections = detector.detect(ast); assertThat(detections).hasSize(2); @@ -77,8 +77,8 @@ void detectWithoutStaticImport() { @Test void detectWithWildcardImport() { JavaParserFacade analysisContext = getAnalysisContext(parser, TEST_FILES_DIR); - CompilationUnit ast = parseAstForFile(TEST_FILES_DIR.resolve("InvokeLaterInvokeAndWaitStaticWildcardImport.java")); detector.setAnalysisContext(analysisContext); + CompilationUnit ast = parseAstForFile(TEST_FILES_DIR.resolve("InvokeLaterInvokeAndWaitStaticWildcardImport.java")); List> detections = detector.detect(ast); assertThat(detections).hasSize(2); @@ -97,8 +97,8 @@ void detectWithWildcardImport() { @Test void detectNoPerfume() { JavaParserFacade analysisContext = getAnalysisContext(parser, TEST_FILES_DIR); - CompilationUnit ast = parseAstForFile(TEST_FILES_DIR.resolve("InvokeLaterInvokeAndWaitNoPerfume.java")); detector.setAnalysisContext(analysisContext); + CompilationUnit ast = parseAstForFile(TEST_FILES_DIR.resolve("InvokeLaterInvokeAndWaitNoPerfume.java")); List> detections = detector.detect(ast); assertThat(detections).isEmpty(); From b18b1b792caccf70852124344fc4c34fd7cb3992 Mon Sep 17 00:00:00 2001 From: chrisknedl Date: Thu, 15 May 2025 15:30:12 +0200 Subject: [PATCH 7/7] increase version number --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 5179a3f..d4cb8d3 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ de.jsilbereisen perfumator-java - 0.4.1 + 0.4.2 jar