Skip to content

Commit

Permalink
add swingutilities perfume
Browse files Browse the repository at this point in the history
  • Loading branch information
Christoph Knoedlseder committed Oct 4, 2024
1 parent a3dddea commit 28c9d57
Show file tree
Hide file tree
Showing 6 changed files with 189 additions and 16 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package de.jsilbereisen.perfumator.engine.detector.perfume;

import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.ImportDeclaration;
import com.github.javaparser.ast.expr.MethodCallExpr;
import com.github.javaparser.symbolsolver.javaparsermodel.JavaParserFacade;
import de.jsilbereisen.perfumator.engine.detector.Detector;
import de.jsilbereisen.perfumator.engine.visitor.MethodCallByNameVisitor;
import de.jsilbereisen.perfumator.model.DetectedInstance;
import de.jsilbereisen.perfumator.model.perfume.Perfume;
import lombok.EqualsAndHashCode;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.*;

@EqualsAndHashCode
public class ThreadSafeSwingDetector implements Detector<Perfume> {

private Perfume perfume;

private JavaParserFacade analysisContext;

private final static String SWING_UTILITIES = "SwingUtilities";
private final static String INVOKE_LATER = "invokeLater";
private final static String INVOKE_AND_WAIT = "invokeAndWait";
private final static Set<String> RELEVANT_METHOD_NAMES = Set.of(INVOKE_LATER, INVOKE_AND_WAIT);
private final static Map<String, String> RELEVANT_IMPORTS =
Map.of(INVOKE_LATER, "javax.swing.SwingUtilities.invokeLater",
INVOKE_AND_WAIT, "javax.swing.SwingUtilities.invokeAndWait");

@Override
public @NotNull List<DetectedInstance<Perfume>> detect(@NotNull CompilationUnit astRoot) {
List<DetectedInstance<Perfume>> detectedInstances = new ArrayList<>();
Set<String> staticImports = getStaticImports(astRoot);
List<MethodCallExpr> methodCalls = getInvokeLaterInvokeAndWaitMethodCalls(astRoot, staticImports);
methodCalls.forEach(callExpr -> detectedInstances.add(DetectedInstance.from(callExpr, perfume, astRoot)));
return detectedInstances;
}

@Override
public void setConcreteDetectable(@NotNull Perfume concreteDetectable) {
this.perfume = concreteDetectable;
}

@Override
public void setAnalysisContext(@Nullable JavaParserFacade analysisContext) {
this.analysisContext = analysisContext;
}

private Set<String> getStaticImports(@NotNull CompilationUnit astRoot) {
Set<String> importedAnnotations = new HashSet<>();

for (ImportDeclaration importDeclaration : astRoot.getImports()) {
for (Map.Entry<String, String> annotationToImport : RELEVANT_IMPORTS.entrySet()) {
if (importDeclaration.getNameAsString().equals(annotationToImport.getValue())) {
importedAnnotations.add(annotationToImport.getKey());
}
}
}
return importedAnnotations;
}

private List<MethodCallExpr> getInvokeLaterInvokeAndWaitMethodCalls(@NotNull CompilationUnit astRoot,
Set<String> imports) {
MethodCallByNameVisitor methodCallByNameVisitor = new MethodCallByNameVisitor();
astRoot.accept(methodCallByNameVisitor, null);
List<MethodCallExpr> relevantMethodCallExpressions = new ArrayList<>();
for (MethodCallExpr methodCallExpr : methodCallByNameVisitor.getMethodCalls()) {
if (RELEVANT_METHOD_NAMES.contains(methodCallExpr.getNameAsString())) {
if (isPartOfSwingUtilities(methodCallExpr)) {
relevantMethodCallExpressions.add(methodCallExpr);
} else if (imports.contains(methodCallExpr.getNameAsString())) {
relevantMethodCallExpressions.add(methodCallExpr);
}
}
}
return relevantMethodCallExpressions;
}

private boolean isPartOfSwingUtilities(MethodCallExpr methodCallExpr) {
var scope = methodCallExpr.getScope();
return scope.map(expression -> expression.toString().equals(SWING_UTILITIES)).orElse(false);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name": "Thread safe Swing",
"description": "Java Swing application make use of the event dispatch thread. This thread must handle the initialization of and update operations on the GUI components, as well as event handling. Event handlers We as programmers are responsible that GUI components are created and updated in the event dispatch thread. We can achieve this by placing the related code inside a Runnable that we pass as an argument to either the \"SwingUtilities.invokeLater\" or the \"SwingUtilities.invokeAndWait\" method.",
"detectorClassSimpleName": "ThreadSafeSwingDetector",
"i18nBaseBundleName": "threadSafeSwing",
"sources": ["Programmierung II at Uni Passau", "https://docs.oracle.com/javase/tutorial/uiswing/concurrency/dispatch.html"],
"relatedPattern": "BUG",
"additionalInformation": null
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import com.github.javaparser.ast.CompilationUnit;
import de.jsilbereisen.perfumator.engine.detector.Detector;
import de.jsilbereisen.perfumator.engine.detector.perfume.ParameterizedTestDetector;
import de.jsilbereisen.perfumator.model.CodeRange;
import de.jsilbereisen.perfumator.model.DetectedInstance;
import de.jsilbereisen.perfumator.model.perfume.Perfume;
Expand All @@ -15,9 +14,9 @@

import static org.assertj.core.api.Assertions.assertThat;

public class AssertAllDetector extends AbstractDetectorTest {

private static final Path TEST_FILE = DEFAULT_DETECTOR_TEST_FILES_DIR.resolve("ParameterizedTests.java");
class AssertAllDetectorTest extends AbstractDetectorTest {
private static final Path TEST_FILE = DEFAULT_DETECTOR_TEST_FILES_DIR.resolve("AssertAllPerfume.java");

private static Perfume perfume;

Expand All @@ -28,9 +27,9 @@ public class AssertAllDetector extends AbstractDetectorTest {
@BeforeAll
static void init() {
perfume = new Perfume();
perfume.setName("Parameterized Test");
perfume.setName("Assert All");

detector = new ParameterizedTestDetector();
detector = new de.jsilbereisen.perfumator.engine.detector.perfume.AssertAllDetector();
detector.setConcreteDetectable(perfume);

ast = parseAstForFile(TEST_FILE);
Expand All @@ -45,7 +44,7 @@ void detect() {
DetectedInstance<Perfume> detection = detections.get(0);

assertThat(detection.getDetectable()).isEqualTo(perfume);
assertThat(detection.getTypeName()).isEqualTo("ParameterizedTests");
assertThat(detection.getCodeRanges()).containsExactly(CodeRange.of(21, 5, 25, 5));
assertThat(detection.getTypeName()).isEqualTo("AssertAllPerfume");
assertThat(detection.getCodeRanges()).containsExactly(CodeRange.of(16, 9, 21, 9));
}
}
16 changes: 8 additions & 8 deletions src/test/java/detectors/ParameterizedTestDetectorTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import com.github.javaparser.ast.CompilationUnit;
import de.jsilbereisen.perfumator.engine.detector.Detector;
import de.jsilbereisen.perfumator.engine.detector.perfume.AssertAllDetector;
import de.jsilbereisen.perfumator.engine.detector.perfume.ParameterizedTestDetector;
import de.jsilbereisen.perfumator.model.CodeRange;
import de.jsilbereisen.perfumator.model.DetectedInstance;
import de.jsilbereisen.perfumator.model.perfume.Perfume;
Expand All @@ -15,9 +15,9 @@

import static org.assertj.core.api.Assertions.assertThat;

class ParameterizedTestDetectorTest extends AbstractDetectorTest {
private static final Path TEST_FILE = DEFAULT_DETECTOR_TEST_FILES_DIR.resolve("AssertAllPerfume.java");
public class ParameterizedTestDetectorTest extends AbstractDetectorTest {

private static final Path TEST_FILE = DEFAULT_DETECTOR_TEST_FILES_DIR.resolve("ParameterizedTests.java");

private static Perfume perfume;

Expand All @@ -28,9 +28,9 @@ class ParameterizedTestDetectorTest extends AbstractDetectorTest {
@BeforeAll
static void init() {
perfume = new Perfume();
perfume.setName("Assert All");
perfume.setName("Parameterized Test");

detector = new AssertAllDetector();
detector = new ParameterizedTestDetector();
detector.setConcreteDetectable(perfume);

ast = parseAstForFile(TEST_FILE);
Expand All @@ -45,7 +45,7 @@ void detect() {
DetectedInstance<Perfume> detection = detections.get(0);

assertThat(detection.getDetectable()).isEqualTo(perfume);
assertThat(detection.getTypeName()).isEqualTo("AssertAllPerfume");
assertThat(detection.getCodeRanges()).containsExactly(CodeRange.of(16, 9, 21, 9));
assertThat(detection.getTypeName()).isEqualTo("ParameterizedTests");
assertThat(detection.getCodeRanges()).containsExactly(CodeRange.of(22, 5, 26, 5));
}
}
56 changes: 56 additions & 0 deletions src/test/java/detectors/ThreadSafeSwingDetectorTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package detectors;

import com.github.javaparser.ast.CompilationUnit;
import de.jsilbereisen.perfumator.engine.detector.Detector;
import de.jsilbereisen.perfumator.engine.detector.perfume.ThreadSafeSwingDetector;
import de.jsilbereisen.perfumator.model.CodeRange;
import de.jsilbereisen.perfumator.model.DetectedInstance;
import de.jsilbereisen.perfumator.model.perfume.Perfume;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import test.AbstractDetectorTest;

import java.nio.file.Path;
import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;

public class ThreadSafeSwingDetectorTest extends AbstractDetectorTest {

private static final Path TEST_FILE =
DEFAULT_DETECTOR_TEST_FILES_DIR.resolve("swing").resolve("InvokeLaterInvokeAndWait.java");

private static Perfume perfume;

private static Detector<Perfume> detector;

private static CompilationUnit ast;

@BeforeAll
static void init() {
perfume = new Perfume();
perfume.setName("Thread safe Swing");

detector = new ThreadSafeSwingDetector();
detector.setConcreteDetectable(perfume);

ast = parseAstForFile(TEST_FILE);
}

@Test
void detect() {
List<DetectedInstance<Perfume>> detections = detector.detect(ast);

assertThat(detections).hasSize(2);

DetectedInstance<Perfume> detection = detections.get(0);
assertThat(detection.getDetectable()).isEqualTo(perfume);
assertThat(detection.getTypeName()).isEqualTo("InvokeLaterInvokeAndWait");
assertThat(detection.getCodeRanges()).containsExactly(CodeRange.of(9, 9, 11, 10));

detection = detections.get(1);
assertThat(detection.getDetectable()).isEqualTo(perfume);
assertThat(detection.getTypeName()).isEqualTo("InvokeLaterInvokeAndWait");
assertThat(detection.getCodeRanges()).containsExactly(CodeRange.of(13, 9, 15, 10));
}
}
24 changes: 24 additions & 0 deletions src/test/resources/detectors/swing/InvokeLaterInvokeAndWait.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package de.jsilbereisen.test;

import javax.swing.SwingUtilities;
import static javax.swing.SwingUtilities.invokeAndWait;

public class InvokeLaterInvokeAndWait {

public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
// perfume
});

invokeAndWait(() -> {
// perfume
});

// no perfume
invokeLater();
}

public static void invokeLater() {
// no perfume, not the library method we are looking for
}
}

0 comments on commit 28c9d57

Please sign in to comment.