diff --git a/autograder-api/src/main/java/de/firemage/autograder/api/ArtemisUtil.java b/autograder-api/src/main/java/de/firemage/autograder/api/ArtemisUtil.java deleted file mode 100644 index 81012609..00000000 --- a/autograder-api/src/main/java/de/firemage/autograder/api/ArtemisUtil.java +++ /dev/null @@ -1,23 +0,0 @@ -package de.firemage.autograder.api; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.stream.Stream; - -public class ArtemisUtil { - private ArtemisUtil() { - - } - - public static Path resolveCodePathEclipseGradingTool(Path submissionRoot) throws IOException { - try (Stream files = Files.list(submissionRoot)) { - return files - .filter(child -> !child.endsWith(".metadata")) - .findAny() - .orElseThrow(() -> new IllegalStateException("No student code found")) - .resolve("assignment") - .resolve("src"); - } - } -} diff --git a/autograder-cmd/src/main/java/de/firemage/autograder/cmd/Application.java b/autograder-cmd/src/main/java/de/firemage/autograder/cmd/Application.java index f6d4d12c..b6bd288f 100644 --- a/autograder-cmd/src/main/java/de/firemage/autograder/cmd/Application.java +++ b/autograder-cmd/src/main/java/de/firemage/autograder/cmd/Application.java @@ -2,7 +2,6 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import de.firemage.autograder.api.ArtemisUtil; import de.firemage.autograder.api.CheckConfiguration; import de.firemage.autograder.api.AbstractCodePosition; import de.firemage.autograder.api.JavaVersion; @@ -14,6 +13,7 @@ import de.firemage.autograder.api.Translatable; import de.firemage.autograder.api.loader.AutograderLoader; import de.firemage.autograder.cmd.output.Annotation; +import de.firemage.autograder.core.integrated.CoreUtil; import de.firemage.autograder.span.Formatter; import de.firemage.autograder.span.Highlight; import de.firemage.autograder.span.Position; @@ -58,10 +58,6 @@ public class Application implements Callable { @Option(names = {"-j", "--java", "--java-version"}, defaultValue = "17", description = "Set the Java version.") private String javaVersion; - @Option(names = { - "--artemis"}, description = "Assume that the given root folder is the workspace root of the grading tool.") - private boolean artemisFolders; - @Option(names = { "--output-json"}, description = "Output the found problems in JSON format instead of more readable plain text") private boolean outputJson; @@ -81,6 +77,9 @@ public class Application implements Callable { @Option(names = {"--max-problems"}, description = "The maximum number of problems to report per check", defaultValue = "10") private int maxProblemsPerCheck; + @Option(names = {"--debug"}, description = "Enables debug mode, note that this slows down execution", defaultValue = "false") + private boolean isInDebugMode; + @Spec private CommandSpec spec; @@ -186,13 +185,21 @@ public Integer call() { throw new ParameterException(this.spec.commandLine(), "Unknown java version '" + javaVersion + "'"); } - if (this.artemisFolders) { - try { - this.file = ArtemisUtil.resolveCodePathEclipseGradingTool(this.file); - } catch (IOException e) { - e.printStackTrace(); - return IO_EXIT_CODE; - } + // Depending on the structure of the project, the code might be in a subdirectory. + // By default, we support explicitly specifying the folder to the first package (./src/main/java) + // + // Here we check if the project has a folder `src/` or `assignment/src/` + // and if so, we assume that the code is in that folder. + if (Files.exists(this.file.resolve("src"))) { + this.file = this.file.resolve("src"); + } + + if (Files.exists(this.file.resolve("assignment/src"))) { + this.file = this.file.resolve("assignment/src"); + } + + if (this.isInDebugMode) { + CoreUtil.setDebugMode(); } if (!outputJson) { diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/CodeModel.java b/autograder-core/src/main/java/de/firemage/autograder/core/CodeModel.java index af1db3ab..2872555f 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/CodeModel.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/CodeModel.java @@ -2,8 +2,8 @@ import de.firemage.autograder.core.file.SourceInfo; import de.firemage.autograder.core.integrated.MethodHierarchy; +import de.firemage.autograder.core.integrated.MethodUtil; import de.firemage.autograder.core.integrated.ModelBuildException; -import de.firemage.autograder.core.integrated.SpoonUtil; import de.firemage.autograder.core.integrated.UsesFinder; import spoon.Launcher; import spoon.compiler.Environment; @@ -98,7 +98,7 @@ public CtMethod findMain() { this.mainMethod = this.getModel() .getElements(new NamedElementFilter<>(CtMethod.class, "main")) .stream() - .filter(SpoonUtil::isMainMethod) + .filter(MethodUtil::isMainMethod) .findFirst() .map(ctMethod -> (CtMethod) ctMethod); } diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/ProblemType.java b/autograder-core/src/main/java/de/firemage/autograder/core/ProblemType.java index c7c659a2..279c6232 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/ProblemType.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/ProblemType.java @@ -3,9 +3,6 @@ import de.firemage.autograder.api.AbstractProblemType; import de.firemage.autograder.api.HasFalsePositives; -import java.util.Collection; -import java.util.List; - public enum ProblemType implements AbstractProblemType { /** * If the code is split into multiple packages, all input must happen in one package. Otherwise, one class must do all input. @@ -342,7 +339,7 @@ public enum ProblemType implements AbstractProblemType { * Reports code where the Collection#addAll method could be used. */ @HasFalsePositives - COLLECTION_ADD_ALL, + SEQUENTIAL_ADD_ALL, /** * Reports cases where Pattern#compile is not in a static final field. @@ -675,16 +672,17 @@ public enum ProblemType implements AbstractProblemType { COMMON_REIMPLEMENTATION_HYPOT, /** - * Reports code where the Collection#addAll method could be used. + * Reports loops where a method is repeatedly called like Collection#add, + * which could be replaced with a single call to Collection#addAll. */ @HasFalsePositives - COMMON_REIMPLEMENTATION_ADD_ALL, + FOR_LOOP_CAN_BE_INVOCATION, /** * Reports code where Enum#values() could be used. */ @HasFalsePositives - COMMON_REIMPLEMENTATION_ADD_ENUM_VALUES, + USE_ENUM_VALUES, /** * Reports code where Arrays#fill could be used. diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/check/api/AvoidStringConcat.java b/autograder-core/src/main/java/de/firemage/autograder/core/check/api/AvoidStringConcat.java index 68a7708d..f438dbf6 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/check/api/AvoidStringConcat.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/check/api/AvoidStringConcat.java @@ -5,7 +5,7 @@ import de.firemage.autograder.core.ProblemType; import de.firemage.autograder.core.check.ExecutableCheck; import de.firemage.autograder.core.integrated.IntegratedCheck; -import de.firemage.autograder.core.integrated.SpoonUtil; +import de.firemage.autograder.core.integrated.MethodUtil; import de.firemage.autograder.core.integrated.StaticAnalysis; import spoon.processing.AbstractProcessor; import spoon.reflect.code.CtInvocation; @@ -26,7 +26,7 @@ public void process(CtInvocation ctInvocation) { if (ctInvocation.getTarget() == null || ctInvocation.getTarget().getType() == null - || !SpoonUtil.isSignatureEqualTo(ctInvocation.getExecutable(), String.class, "concat", String.class)) { + || !MethodUtil.isSignatureEqualTo(ctInvocation.getExecutable(), String.class, "concat", String.class)) { return; } diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/check/api/CharRange.java b/autograder-core/src/main/java/de/firemage/autograder/core/check/api/CharRange.java index ff5f7c9b..69904e6e 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/check/api/CharRange.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/check/api/CharRange.java @@ -4,9 +4,11 @@ import de.firemage.autograder.core.ProblemType; import de.firemage.autograder.core.check.ExecutableCheck; import de.firemage.autograder.core.integrated.CtRange; +import de.firemage.autograder.core.integrated.ExpressionUtil; +import de.firemage.autograder.core.integrated.FactoryUtil; import de.firemage.autograder.core.integrated.IntegratedCheck; -import de.firemage.autograder.core.integrated.SpoonUtil; import de.firemage.autograder.core.integrated.StaticAnalysis; +import de.firemage.autograder.core.integrated.TypeUtil; import org.apache.commons.lang3.Range; import spoon.processing.AbstractProcessor; import spoon.reflect.code.BinaryOperatorKind; @@ -39,35 +41,35 @@ default CtExpression suggest(CtExpression ctExpression) { private static final Map, Suggester> MAPPING = Map.of( Range.of('a', 'z'), (factory, ctExpression, targetType) -> factory.createBinaryOperator( - SpoonUtil.createStaticInvocation( + FactoryUtil.createStaticInvocation( targetType, "isAlphabetic", - SpoonUtil.castExpression(int.class, ctExpression) + ExpressionUtil.castExpression(int.class, ctExpression) ), - SpoonUtil.createStaticInvocation( + FactoryUtil.createStaticInvocation( targetType, "isLowerCase", - SpoonUtil.castExpression(char.class, ctExpression) + ExpressionUtil.castExpression(char.class, ctExpression) ), BinaryOperatorKind.AND ), Range.of('A', 'Z'), (factory, ctExpression, targetType) -> factory.createBinaryOperator( - SpoonUtil.createStaticInvocation( + FactoryUtil.createStaticInvocation( targetType, "isAlphabetic", - SpoonUtil.castExpression(int.class, ctExpression) + ExpressionUtil.castExpression(int.class, ctExpression) ), - SpoonUtil.createStaticInvocation( + FactoryUtil.createStaticInvocation( targetType, "isUpperCase", - SpoonUtil.castExpression(char.class, ctExpression) + ExpressionUtil.castExpression(char.class, ctExpression) ), BinaryOperatorKind.AND ), - Range.of('0', '9'), (factory, ctExpression, targetType) -> SpoonUtil.createStaticInvocation( + Range.of('0', '9'), (factory, ctExpression, targetType) -> FactoryUtil.createStaticInvocation( targetType, "isDigit", - SpoonUtil.castExpression(char.class, ctExpression) + ExpressionUtil.castExpression(char.class, ctExpression) ) ); @@ -87,7 +89,7 @@ protected void check(StaticAnalysis staticAnalysis) { public void process(CtBinaryOperator ctBinaryOperator) { if (ctBinaryOperator.isImplicit() || !ctBinaryOperator.getPosition().isValidPosition() - || !SpoonUtil.isTypeEqualTo(ctBinaryOperator.getType(), java.lang.Boolean.class, boolean.class)) { + || !TypeUtil.isTypeEqualTo(ctBinaryOperator.getType(), java.lang.Boolean.class, boolean.class)) { return; } @@ -95,7 +97,7 @@ public void process(CtBinaryOperator ctBinaryOperator) { CtBinaryOperator operator = ctBinaryOperator; if (ctBinaryOperator.getKind() == BinaryOperatorKind.OR) { isNegated = true; - operator = (CtBinaryOperator) SpoonUtil.negate(ctBinaryOperator); + operator = (CtBinaryOperator) ExpressionUtil.negate(ctBinaryOperator); } else { isNegated = false; if (ctBinaryOperator.getKind() != BinaryOperatorKind.AND) { @@ -132,7 +134,7 @@ public void process(CtBinaryOperator ctBinaryOperator) { makeSuggestion(leftRange.ctExpression(), intersection).ifPresent(suggestion -> { if (isNegated) { - suggestion = SpoonUtil.negate(suggestion); + suggestion = ExpressionUtil.negate(suggestion); } addLocalProblem( diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/check/api/CheckIterableDuplicates.java b/autograder-core/src/main/java/de/firemage/autograder/core/check/api/CheckIterableDuplicates.java index 6ac21f14..66044d7c 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/check/api/CheckIterableDuplicates.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/check/api/CheckIterableDuplicates.java @@ -4,8 +4,9 @@ import de.firemage.autograder.core.ProblemType; import de.firemage.autograder.core.check.ExecutableCheck; import de.firemage.autograder.core.integrated.IntegratedCheck; -import de.firemage.autograder.core.integrated.SpoonUtil; +import de.firemage.autograder.core.integrated.StatementUtil; import de.firemage.autograder.core.integrated.StaticAnalysis; +import de.firemage.autograder.core.integrated.TypeUtil; import spoon.processing.AbstractProcessor; import spoon.reflect.code.CtAssignment; import spoon.reflect.code.CtBreak; @@ -53,7 +54,7 @@ public void process(CtForEach ctForEach) { return; } - List statements = SpoonUtil.getEffectiveStatements(ctForEach.getBody()); + List statements = StatementUtil.getEffectiveStatements(ctForEach.getBody()); if (statements.size() != 1 || !(statements.get(0) instanceof CtIf ctIf)) { return; } @@ -63,7 +64,7 @@ public void process(CtForEach ctForEach) { return; } - List ifStatements = SpoonUtil.getEffectiveStatements(ctIf.getThenStatement()); + List ifStatements = StatementUtil.getEffectiveStatements(ctIf.getThenStatement()); if (ifStatements.isEmpty()) { return; } @@ -92,12 +93,12 @@ public void process(CtForEach ctForEach) { if (!(ctIf.getCondition() instanceof CtUnaryOperator ctUnaryOperator && ctUnaryOperator.getKind() == UnaryOperatorKind.NOT && ctUnaryOperator.getOperand() instanceof CtInvocation ctInvocation - && SpoonUtil.isTypeEqualTo(ctInvocation.getExecutable().getType(), boolean.class) + && TypeUtil.isTypeEqualTo(ctInvocation.getExecutable().getType(), boolean.class) && ctInvocation.getExecutable().getSimpleName().equals("add") && ctInvocation.getArguments().size() == 1 && ctInvocation.getArguments().get(0) instanceof CtVariableRead ctVariableRead && ctVariableRead.getVariable().equals(ctForEach.getVariable().getReference()) - && SpoonUtil.isSubtypeOf(ctInvocation.getTarget().getType(), java.util.Set.class))) + && TypeUtil.isSubtypeOf(ctInvocation.getTarget().getType(), java.util.Set.class))) { return; } diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/check/api/CollectionAddAll.java b/autograder-core/src/main/java/de/firemage/autograder/core/check/api/CollectionAddAll.java deleted file mode 100644 index 5b4f13e0..00000000 --- a/autograder-core/src/main/java/de/firemage/autograder/core/check/api/CollectionAddAll.java +++ /dev/null @@ -1,255 +0,0 @@ -package de.firemage.autograder.core.check.api; - -import de.firemage.autograder.core.LocalizedMessage; -import de.firemage.autograder.core.ProblemType; -import de.firemage.autograder.core.check.ExecutableCheck; -import de.firemage.autograder.core.check.api.UseEnumValues.CtEnumFieldRead; -import de.firemage.autograder.core.integrated.IntegratedCheck; -import de.firemage.autograder.core.integrated.SpoonUtil; -import de.firemage.autograder.core.integrated.StaticAnalysis; -import spoon.reflect.code.CtBlock; -import spoon.reflect.code.CtExpression; -import spoon.reflect.code.CtForEach; -import spoon.reflect.code.CtInvocation; -import spoon.reflect.code.CtStatement; -import spoon.reflect.code.CtVariableAccess; -import spoon.reflect.code.CtVariableRead; -import spoon.reflect.declaration.CtElement; -import spoon.reflect.declaration.CtEnum; -import spoon.reflect.declaration.CtEnumValue; -import spoon.reflect.declaration.CtType; -import spoon.reflect.declaration.CtVariable; -import spoon.reflect.reference.CtExecutableReference; -import spoon.reflect.reference.CtTypeParameterReference; -import spoon.reflect.reference.CtTypeReference; -import spoon.reflect.reference.CtVariableReference; -import spoon.reflect.visitor.CtScanner; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -@ExecutableCheck(reportedProblems = { - ProblemType.COLLECTION_ADD_ALL, - ProblemType.COMMON_REIMPLEMENTATION_ADD_ENUM_VALUES, - ProblemType.COMMON_REIMPLEMENTATION_ADD_ALL -}) -public class CollectionAddAll extends IntegratedCheck { - private static final int MIN_ADD_ALL_SIZE = 4; - - private record AddInvocation( - CtVariableReference collection, - CtExecutableReference executableReference, - CtExpression value - ) { - public static Optional of(CtStatement ctStatement) { - CtType collectionType = ctStatement.getFactory().Type().get(java.util.Collection.class); - if (!(ctStatement instanceof CtInvocation ctInvocation) - || !(ctInvocation.getTarget() instanceof CtVariableAccess ctVariableAccess) - || ctVariableAccess.getVariable().getType() instanceof CtTypeParameterReference - || !ctVariableAccess.getVariable().getType().isSubtypeOf(collectionType.getReference())) { - return Optional.empty(); - } - - CtExecutableReference executableReference = ctInvocation.getExecutable(); - CtVariableReference collection = ctVariableAccess.getVariable(); - if (!SpoonUtil.isSignatureEqualTo( - executableReference, - boolean.class, - "add", - Object.class)) { - return Optional.empty(); - } - - return Optional.of(new AddInvocation(collection, executableReference, ctInvocation.getArguments().get(0))); - } - } - - private static boolean isOrderedCollection(CtTypeReference ctTypeReference) { - return Stream.of(java.util.List.class) - .map(ctClass -> ctTypeReference.getFactory().createCtTypeReference(ctClass)) - .anyMatch(ctTypeReference::isSubtypeOf); - } - - private void reportProblem(CtVariable ctVariable, List> addedValues) { - String values = "List.of(%s)".formatted(addedValues.stream() - .map(CtElement::toString) - .collect(Collectors.joining(", ")) - ); - - List> fieldReads = new ArrayList<>(); - CtEnum ctEnum = null; - for (CtExpression value : addedValues) { - CtEnumFieldRead fieldRead = CtEnumFieldRead.of(value).orElse(null); - if (fieldRead == null) { - ctEnum = null; - break; - } - - if (ctEnum == null) { - ctEnum = fieldRead.ctEnum(); - } - - if (!ctEnum.equals(fieldRead.ctEnum())) { - ctEnum = null; - break; - } - - fieldReads.add(fieldRead.ctEnumValue()); - } - - if (ctEnum != null && UseEnumValues.checkEnumValues(ctEnum, isOrderedCollection(ctVariable.getType()), fieldReads)) { - addLocalProblem( - addedValues.get(0).getParent(CtStatement.class), - new LocalizedMessage( - "common-reimplementation", - Map.of( - "suggestion", "%s.addAll(Arrays.asList(%s.values()))".formatted( - ctVariable.getSimpleName(), - ctEnum.getSimpleName() - ) - ) - ), - ProblemType.COMMON_REIMPLEMENTATION_ADD_ENUM_VALUES - ); - return; - } - - if (addedValues.size() < MIN_ADD_ALL_SIZE) { - return; - } - - - addLocalProblem( - addedValues.get(0).getParent(CtStatement.class), - new LocalizedMessage( - "common-reimplementation", - Map.of( - "suggestion", "%s.addAll(%s)".formatted(ctVariable.getSimpleName(), values) - ) - ), - ProblemType.COLLECTION_ADD_ALL - ); - } - - private void checkAddAll(CtBlock ctBlock) { - List statements = SpoonUtil.getEffectiveStatements(ctBlock); - - CtVariableReference collection = null; - List> addedValues = new ArrayList<>(); - - for (CtStatement ctStatement : statements) { - AddInvocation addInvocation = AddInvocation.of(ctStatement).orElse(null); - if (addInvocation == null) { - if (addedValues.size() > 1 && collection != null) { - reportProblem(collection.getDeclaration(), addedValues); - } - - collection = null; - continue; - } - - if (collection == null) { - collection = addInvocation.collection(); - addedValues.clear(); - } - - // ensure that all invocations refer to the same collection - if (!collection.equals(addInvocation.collection())) { - if (addedValues.size() > 1) { - reportProblem(collection.getDeclaration(), addedValues); - } - - // if there are multiple collections, just invalidate the data - collection = null; - continue; - } - - addedValues.add(addInvocation.value()); - } - - if (addedValues.size() > 1 && collection != null) { - reportProblem(collection.getDeclaration(), addedValues); - } - } - - private void checkAddAll(CtForEach ctFor) { - List statements = SpoonUtil.getEffectiveStatements(ctFor.getBody()); - if (statements.size() != 1) { - return; - } - - if (statements.get(0) instanceof CtInvocation ctInvocation - && SpoonUtil.isSubtypeOf(ctInvocation.getTarget().getType(), java.util.Collection.class) - && SpoonUtil.isSignatureEqualTo(ctInvocation.getExecutable(), boolean.class, "add", Object.class) - && ctInvocation.getExecutable().getParameters().size() == 1 - // ensure that the add argument simply accesses the for variable: - // for (T t : array) { - // collection.add(t); - // } - && ctInvocation.getArguments().get(0) instanceof CtVariableRead ctVariableRead - && ctVariableRead.getVariable().equals(ctFor.getVariable().getReference())) { - - // allow explicit casting - if (!ctInvocation.getArguments().get(0).getTypeCasts().isEmpty()) { - return; - } - - // handle edge case where the variable is implicitly cast in the invocation (Character in List, but char in iterable) - List> actualTypeArguments = ctInvocation.getTarget().getType().getActualTypeArguments(); - if (!actualTypeArguments.isEmpty() && !ctFor.getVariable().getType().equals(actualTypeArguments.get(0))) { - return; - } - - String addAllArg = ctFor.getExpression().toString(); - if (ctFor.getExpression().getType().isArray()) { - addAllArg = "Arrays.asList(%s)".formatted(addAllArg); - } - - - this.addLocalProblem( - ctFor, - new LocalizedMessage( - "common-reimplementation", - Map.of( - "suggestion", "%s.addAll(%s)".formatted( - ctInvocation.getTarget(), - addAllArg - ) - ) - ), - ProblemType.COMMON_REIMPLEMENTATION_ADD_ALL - ); - } - } - - @Override - protected void check(StaticAnalysis staticAnalysis) { - staticAnalysis.getModel().getRootPackage().accept(new CtScanner() { - @Override - public void visitCtBlock(CtBlock ctBlock) { - if (ctBlock.isImplicit() || !ctBlock.getPosition().isValidPosition()) { - super.visitCtBlock(ctBlock); - return; - } - - checkAddAll(ctBlock); - super.visitCtBlock(ctBlock); - } - - @Override - public void visitCtForEach(CtForEach ctFor) { - if (ctFor.isImplicit() || !ctFor.getPosition().isValidPosition()) { - super.visitCtForEach(ctFor); - return; - } - - checkAddAll(ctFor); - super.visitCtForEach(ctFor); - } - }); - } -} diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/check/api/CollectionsNCopies.java b/autograder-core/src/main/java/de/firemage/autograder/core/check/api/CollectionsNCopies.java index b779076e..58ccf03a 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/check/api/CollectionsNCopies.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/check/api/CollectionsNCopies.java @@ -5,8 +5,9 @@ import de.firemage.autograder.core.check.ExecutableCheck; import de.firemage.autograder.core.integrated.ForLoopRange; import de.firemage.autograder.core.integrated.IntegratedCheck; -import de.firemage.autograder.core.integrated.SpoonUtil; +import de.firemage.autograder.core.integrated.StatementUtil; import de.firemage.autograder.core.integrated.StaticAnalysis; +import de.firemage.autograder.core.integrated.TypeUtil; import spoon.processing.AbstractProcessor; import spoon.reflect.code.CtExpression; import spoon.reflect.code.CtFor; @@ -31,7 +32,7 @@ public void process(CtFor ctFor) { ForLoopRange forLoopRange = ForLoopRange.fromCtFor(ctFor).orElse(null); - List statements = SpoonUtil.getEffectiveStatements(ctFor.getBody()); + List statements = StatementUtil.getEffectiveStatements(ctFor.getBody()); if (statements.size() != 1 || forLoopRange == null @@ -39,7 +40,7 @@ public void process(CtFor ctFor) { || !(ctInvocation.getExecutable().getSimpleName().equals("add")) || ctInvocation.getArguments().size() != 1 || !(ctInvocation.getTarget() instanceof CtVariableRead ctVariableRead) - || !SpoonUtil.isSubtypeOf(ctVariableRead.getType(), java.util.Collection.class)) { + || !TypeUtil.isSubtypeOf(ctVariableRead.getType(), java.util.Collection.class)) { return; } @@ -49,7 +50,7 @@ public void process(CtFor ctFor) { } CtExpression rhs = ctInvocation.getArguments().get(0); - if (!SpoonUtil.isImmutable(rhs.getType())) { + if (!TypeUtil.isImmutable(rhs.getType())) { return; } diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/check/api/ForLoopCanBeInvocation.java b/autograder-core/src/main/java/de/firemage/autograder/core/check/api/ForLoopCanBeInvocation.java new file mode 100644 index 00000000..ab6d49b5 --- /dev/null +++ b/autograder-core/src/main/java/de/firemage/autograder/core/check/api/ForLoopCanBeInvocation.java @@ -0,0 +1,82 @@ +package de.firemage.autograder.core.check.api; + +import de.firemage.autograder.core.LocalizedMessage; +import de.firemage.autograder.core.ProblemType; +import de.firemage.autograder.core.check.ExecutableCheck; +import de.firemage.autograder.core.integrated.IntegratedCheck; +import de.firemage.autograder.core.integrated.StatementUtil; +import de.firemage.autograder.core.integrated.StaticAnalysis; +import de.firemage.autograder.core.integrated.MethodUtil; +import de.firemage.autograder.core.integrated.TypeUtil; +import spoon.processing.AbstractProcessor; +import spoon.reflect.code.CtForEach; +import spoon.reflect.code.CtInvocation; +import spoon.reflect.code.CtStatement; +import spoon.reflect.code.CtVariableRead; +import spoon.reflect.reference.CtTypeReference; + +import java.util.List; +import java.util.Map; + +@ExecutableCheck(reportedProblems = { ProblemType.FOR_LOOP_CAN_BE_INVOCATION }) +public class ForLoopCanBeInvocation extends IntegratedCheck { + @Override + protected void check(StaticAnalysis staticAnalysis) { + staticAnalysis.processWith(new AbstractProcessor() { + @Override + public void process(CtForEach ctFor) { + if (ctFor.isImplicit() || !ctFor.getPosition().isValidPosition()) { + return; + } + + List statements = StatementUtil.getEffectiveStatements(ctFor.getBody()); + if (statements.size() != 1) { + return; + } + + if (statements.get(0) instanceof CtInvocation ctInvocation + && TypeUtil.isSubtypeOf(ctInvocation.getTarget().getType(), java.util.Collection.class) + && MethodUtil.isSignatureEqualTo(ctInvocation.getExecutable(), boolean.class, "add", Object.class) + && ctInvocation.getExecutable().getParameters().size() == 1 + // ensure that the add argument simply accesses the for variable: + // for (T t : array) { + // collection.add(t); + // } + && ctInvocation.getArguments().get(0) instanceof CtVariableRead ctVariableRead + && ctVariableRead.getVariable().equals(ctFor.getVariable().getReference())) { + + // allow explicit casting + if (!ctInvocation.getArguments().get(0).getTypeCasts().isEmpty()) { + return; + } + + // handle edge case where the variable is implicitly cast in the invocation (Character in List, but char in iterable) + List> actualTypeArguments = ctInvocation.getTarget().getType().getActualTypeArguments(); + if (!actualTypeArguments.isEmpty() && !ctFor.getVariable().getType().equals(actualTypeArguments.get(0))) { + return; + } + + String addAllArg = ctFor.getExpression().toString(); + if (ctFor.getExpression().getType().isArray()) { + addAllArg = "Arrays.asList(%s)".formatted(addAllArg); + } + + + addLocalProblem( + ctFor, + new LocalizedMessage( + "common-reimplementation", + Map.of( + "suggestion", "%s.addAll(%s)".formatted( + ctInvocation.getTarget(), + addAllArg + ) + ) + ), + ProblemType.FOR_LOOP_CAN_BE_INVOCATION + ); + } + } + }); + } +} diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/check/api/ImplementComparable.java b/autograder-core/src/main/java/de/firemage/autograder/core/check/api/ImplementComparable.java index f4eb13cf..e5c732b8 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/check/api/ImplementComparable.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/check/api/ImplementComparable.java @@ -4,8 +4,8 @@ import de.firemage.autograder.core.ProblemType; import de.firemage.autograder.core.check.ExecutableCheck; import de.firemage.autograder.core.integrated.IntegratedCheck; -import de.firemage.autograder.core.integrated.SpoonUtil; import de.firemage.autograder.core.integrated.StaticAnalysis; +import de.firemage.autograder.core.integrated.TypeUtil; import spoon.processing.AbstractProcessor; import spoon.reflect.declaration.CtClass; import spoon.reflect.declaration.CtType; @@ -20,7 +20,7 @@ public class ImplementComparable extends IntegratedCheck { private static CtTypeReference getInterface(CtTypeInformation ctType, Class interfaceType) { return ctType.getSuperInterfaces() .stream() - .filter(type -> SpoonUtil.isSubtypeOf(type, interfaceType)) + .filter(type -> TypeUtil.isSubtypeOf(type, interfaceType)) .findAny() .orElse(null); } diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/check/api/IsEmptyReimplementationCheck.java b/autograder-core/src/main/java/de/firemage/autograder/core/check/api/IsEmptyReimplementationCheck.java index fafe50d1..abd61107 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/check/api/IsEmptyReimplementationCheck.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/check/api/IsEmptyReimplementationCheck.java @@ -3,9 +3,11 @@ import de.firemage.autograder.core.LocalizedMessage; import de.firemage.autograder.core.ProblemType; import de.firemage.autograder.core.check.ExecutableCheck; +import de.firemage.autograder.core.integrated.ExpressionUtil; import de.firemage.autograder.core.integrated.IntegratedCheck; -import de.firemage.autograder.core.integrated.SpoonUtil; import de.firemage.autograder.core.integrated.StaticAnalysis; +import de.firemage.autograder.core.integrated.MethodUtil; +import de.firemage.autograder.core.integrated.TypeUtil; import spoon.processing.AbstractProcessor; import spoon.reflect.code.CtBinaryOperator; import spoon.reflect.code.CtExpression; @@ -30,7 +32,7 @@ private void reportProblem(CtElement ctElement, String original, String suggesti } private static boolean isTargetTypeEqualTo(CtInvocation ctInvocation, Class ctClass) { - return ctInvocation.getTarget() != null && SpoonUtil.isTypeEqualTo(ctInvocation.getTarget().getType(), ctClass); + return ctInvocation.getTarget() != null && TypeUtil.isTypeEqualTo(ctInvocation.getTarget().getType(), ctClass); } private static boolean isSizeCall(CtInvocation ctInvocation) { @@ -42,17 +44,17 @@ private static boolean isSizeCall(CtInvocation ctInvocation) { ctInvocation.getFactory().Type().booleanPrimitiveType(), "isEmpty" ) != null - && SpoonUtil.isSignatureEqualTo(ctInvocation.getExecutable(), int.class, "size"); + && MethodUtil.isSignatureEqualTo(ctInvocation.getExecutable(), int.class, "size"); } private static boolean isLengthCall(CtInvocation ctInvocation) { return isTargetTypeEqualTo(ctInvocation, java.lang.String.class) - && SpoonUtil.isSignatureEqualTo(ctInvocation.getExecutable(), int.class, "length"); + && MethodUtil.isSignatureEqualTo(ctInvocation.getExecutable(), int.class, "length"); } private static boolean isEqualsCall(CtInvocation ctInvocation) { return isTargetTypeEqualTo(ctInvocation, java.lang.String.class) - && SpoonUtil.isSignatureEqualTo(ctInvocation.getExecutable(), boolean.class, "equals", Object.class); + && MethodUtil.isSignatureEqualTo(ctInvocation.getExecutable(), boolean.class, "equals", Object.class); } private static CtExpression buildIsEmptySuggestion(CtExpression target) { @@ -69,7 +71,7 @@ private static CtExpression buildIsEmptySuggestion(CtExpression target) { private void checkIsEmptyReimplementation(CtExpression target, CtBinaryOperator ctBinaryOperator, ProblemType problemType) { Predicate> isLiteral = expr -> expr instanceof CtLiteral; - if (!SpoonUtil.isBoolean(ctBinaryOperator)) { + if (!ExpressionUtil.isBoolean(ctBinaryOperator)) { return; } @@ -115,12 +117,12 @@ private void checkIsEmptyReimplementation(CtExpression target, CtBinaryOperat // f() == 0 : isEmpty - CtBinaryOperator result = SpoonUtil.normalizeBy( - (left, right) -> isLiteral.test(SpoonUtil.resolveConstant(left)) && !isLiteral.test(SpoonUtil.resolveConstant(right)), + CtBinaryOperator result = ExpressionUtil.normalizeBy( + (left, right) -> isLiteral.test(ExpressionUtil.resolveConstant(left)) && !isLiteral.test(ExpressionUtil.resolveConstant(right)), ctBinaryOperator ); - if (!(result.getRightHandOperand() instanceof CtLiteral ctLiteral) || !(ctLiteral.getValue() instanceof Number number) || SpoonUtil.isNullLiteral(ctLiteral)) { + if (!(result.getRightHandOperand() instanceof CtLiteral ctLiteral) || !(ctLiteral.getValue() instanceof Number number) || ExpressionUtil.isNullLiteral(ctLiteral)) { return; } @@ -140,13 +142,13 @@ private void checkIsEmptyReimplementation(CtExpression target, CtBinaryOperat // f() != 0 : !isEmpty case NE -> { if (isZero) { - this.reportProblem(ctBinaryOperator, ctBinaryOperator.toString(), SpoonUtil.negate(suggestion).toString(), problemType); + this.reportProblem(ctBinaryOperator, ctBinaryOperator.toString(), ExpressionUtil.negate(suggestion).toString(), problemType); } } // f() >= 1 : !isEmpty case GE -> { if (isOne) { - this.reportProblem(ctBinaryOperator, ctBinaryOperator.toString(), SpoonUtil.negate(suggestion).toString(), problemType); + this.reportProblem(ctBinaryOperator, ctBinaryOperator.toString(), ExpressionUtil.negate(suggestion).toString(), problemType); } } } @@ -155,17 +157,17 @@ private void checkIsEmptyReimplementation(CtExpression target, CtBinaryOperat private void checkEqualsCall(CtExpression target, CtInvocation ctInvocation, ProblemType problemType) { CtExpression argument = ctInvocation.getArguments().get(0); - if (SpoonUtil.isNullLiteral(argument)) { + if (ExpressionUtil.isNullLiteral(argument)) { return; } - if (SpoonUtil.isStringLiteral(SpoonUtil.resolveConstant(argument), "")) { + if (ExpressionUtil.isStringLiteral(ExpressionUtil.resolveConstant(argument), "")) { this.reportProblem(ctInvocation, ctInvocation.toString(), buildIsEmptySuggestion(target).toString(), problemType); return; } // detect "".equals(s) - if (SpoonUtil.isStringLiteral(SpoonUtil.resolveConstant(target), "")) { + if (ExpressionUtil.isStringLiteral(ExpressionUtil.resolveConstant(target), "")) { this.reportProblem(ctInvocation, ctInvocation.toString(), buildIsEmptySuggestion(argument).toString(), problemType); } } diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/check/api/MathReimplementation.java b/autograder-core/src/main/java/de/firemage/autograder/core/check/api/MathReimplementation.java index 0642b448..4c4b8e6b 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/check/api/MathReimplementation.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/check/api/MathReimplementation.java @@ -3,9 +3,12 @@ import de.firemage.autograder.core.LocalizedMessage; import de.firemage.autograder.core.ProblemType; import de.firemage.autograder.core.check.ExecutableCheck; +import de.firemage.autograder.core.integrated.ExpressionUtil; import de.firemage.autograder.core.integrated.IntegratedCheck; -import de.firemage.autograder.core.integrated.SpoonUtil; +import de.firemage.autograder.core.integrated.StatementUtil; import de.firemage.autograder.core.integrated.StaticAnalysis; +import de.firemage.autograder.core.integrated.MethodUtil; +import de.firemage.autograder.core.integrated.TypeUtil; import spoon.reflect.code.BinaryOperatorKind; import spoon.reflect.code.CtAssignment; import spoon.reflect.code.CtBinaryOperator; @@ -35,20 +38,20 @@ public class MathReimplementation extends IntegratedCheck { private static boolean isMathPow(CtInvocation ctInvocation) { return ctInvocation.getTarget() instanceof CtTypeAccess ctTypeAccess - && SpoonUtil.isTypeEqualTo(ctTypeAccess.getAccessedType(), Math.class) - && SpoonUtil.isSignatureEqualTo(ctInvocation.getExecutable(), double.class, "pow", double.class, double.class); + && TypeUtil.isTypeEqualTo(ctTypeAccess.getAccessedType(), Math.class) + && MethodUtil.isSignatureEqualTo(ctInvocation.getExecutable(), double.class, "pow", double.class, double.class); } private static boolean isMathSqrt(CtInvocation ctInvocation) { return ctInvocation.getTarget() instanceof CtTypeAccess ctTypeAccess - && SpoonUtil.isTypeEqualTo(ctTypeAccess.getAccessedType(), Math.class) - && SpoonUtil.isSignatureEqualTo(ctInvocation.getExecutable(), double.class, "sqrt", double.class); + && TypeUtil.isTypeEqualTo(ctTypeAccess.getAccessedType(), Math.class) + && MethodUtil.isSignatureEqualTo(ctInvocation.getExecutable(), double.class, "sqrt", double.class); } private static boolean isPowSqrt(CtInvocation ctInvocation) { return isMathPow(ctInvocation) && ctInvocation.getArguments().size() == 2 - && SpoonUtil.resolveConstant(ctInvocation.getArguments().get(1)) instanceof CtLiteral ctLiteral + && ExpressionUtil.resolveConstant(ctInvocation.getArguments().get(1)) instanceof CtLiteral ctLiteral && ctLiteral.getValue() instanceof Double doubleValue && doubleValue == 0.5; } @@ -122,7 +125,7 @@ private void checkMaxMin(CtIf ctIf) { // ensure that in the if block there is only one assignment to a variable // and the condition is a binary operator with <, <=, > or >= - List thenBlock = SpoonUtil.getEffectiveStatements(ctIf.getThenStatement()); + List thenBlock = StatementUtil.getEffectiveStatements(ctIf.getThenStatement()); if (thenBlock.size() != 1 || !(thenBlock.get(0) instanceof CtAssignment thenAssignment) || !(thenAssignment.getAssigned() instanceof CtVariableWrite ctVariableWrite) @@ -141,7 +144,7 @@ private void checkMaxMin(CtIf ctIf) { assignedVariable.getModifiers().contains(ModifierKind.STATIC) ); if (ctIf.getElseStatement() != null) { - List elseBlock = SpoonUtil.getEffectiveStatements(ctIf.getElseStatement()); + List elseBlock = StatementUtil.getEffectiveStatements(ctIf.getElseStatement()); if (elseBlock.size() != 1 || !(elseBlock.get(0) instanceof CtAssignment elseAssignment) || !(elseAssignment.getAssigned() instanceof CtVariableAccess elseAccess) @@ -156,7 +159,7 @@ private void checkMaxMin(CtIf ctIf) { CtBinaryOperator condition = ctBinaryOperator; // ensure that the else value is on the left side of the condition if (ctBinaryOperator.getRightHandOperand().equals(elseValue)) { - condition = SpoonUtil.swapCtBinaryOperator(condition); + condition = ExpressionUtil.swapCtBinaryOperator(condition); } // if it is not on either side of the condition, return diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/check/api/SequentialAddAll.java b/autograder-core/src/main/java/de/firemage/autograder/core/check/api/SequentialAddAll.java new file mode 100644 index 00000000..d7db6796 --- /dev/null +++ b/autograder-core/src/main/java/de/firemage/autograder/core/check/api/SequentialAddAll.java @@ -0,0 +1,230 @@ +package de.firemage.autograder.core.check.api; + +import de.firemage.autograder.core.LocalizedMessage; +import de.firemage.autograder.core.ProblemType; +import de.firemage.autograder.core.check.ExecutableCheck; +import de.firemage.autograder.core.check.api.UseEnumValues.CtEnumFieldRead; +import de.firemage.autograder.core.integrated.ExpressionUtil; +import de.firemage.autograder.core.integrated.IntegratedCheck; +import de.firemage.autograder.core.integrated.MethodUtil; +import de.firemage.autograder.core.integrated.StatementUtil; +import de.firemage.autograder.core.integrated.StaticAnalysis; +import de.firemage.autograder.core.integrated.TypeUtil; +import spoon.processing.AbstractProcessor; +import spoon.reflect.code.CtBlock; +import spoon.reflect.code.CtExpression; +import spoon.reflect.code.CtInvocation; +import spoon.reflect.code.CtStatement; +import spoon.reflect.code.CtVariableAccess; +import spoon.reflect.declaration.CtElement; +import spoon.reflect.declaration.CtEnum; +import spoon.reflect.declaration.CtEnumValue; +import spoon.reflect.declaration.CtType; +import spoon.reflect.reference.CtExecutableReference; +import spoon.reflect.reference.CtTypeParameterReference; +import spoon.reflect.reference.CtVariableReference; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +@ExecutableCheck(reportedProblems = { + ProblemType.SEQUENTIAL_ADD_ALL, + ProblemType.USE_ENUM_VALUES, +}) +public class SequentialAddAll extends IntegratedCheck { + private static final int MIN_ADD_ALL_SIZE = 4; + + private record AddInvocation( + CtVariableReference collection, + CtExecutableReference executableReference, + CtExpression value + ) { + static Optional of(CtStatement ctStatement) { + CtType collectionType = ctStatement.getFactory().Type().get(java.util.Collection.class); + if (!(ctStatement instanceof CtInvocation ctInvocation) + || !(ctInvocation.getTarget() instanceof CtVariableAccess ctVariableAccess) + || ctVariableAccess.getVariable().getType() instanceof CtTypeParameterReference + || !ctVariableAccess.getVariable().getType().isSubtypeOf(collectionType.getReference())) { + return Optional.empty(); + } + + CtExecutableReference executableReference = ctInvocation.getExecutable(); + CtVariableReference collection = ctVariableAccess.getVariable(); + if (!MethodUtil.isSignatureEqualTo( + executableReference, + boolean.class, + "add", + Object.class)) { + return Optional.empty(); + } + + return Optional.of(new AddInvocation(collection, executableReference, ctInvocation.getArguments().get(0))); + } + } + + private void reportProblem(CtVariableReference ctVariable, List> values) { + // check if the given values are enum values and find the enum they belong to + Collection> fieldReads = new ArrayList<>(); + CtEnum ctEnum = null; + for (CtExpression value : values) { + CtEnumFieldRead fieldRead = CtEnumFieldRead.of(value).orElse(null); + if (fieldRead == null) { + ctEnum = null; + break; + } + + if (ctEnum == null) { + ctEnum = fieldRead.ctEnum(); + } + + if (!ctEnum.equals(fieldRead.ctEnum())) { + ctEnum = null; + break; + } + + fieldReads.add(fieldRead.ctEnumValue()); + } + + if (ctEnum != null && UseEnumValues.checkEnumValues(ctEnum, TypeUtil.isOrderedCollection(ctVariable.getType()), fieldReads)) { + addLocalProblem( + values.get(0).getParent(CtStatement.class), + new LocalizedMessage( + "common-reimplementation", + Map.of( + "suggestion", "%s.addAll(Arrays.asList(%s.values()))".formatted( + ctVariable.getSimpleName(), + ctEnum.getSimpleName() + ) + ) + ), + ProblemType.USE_ENUM_VALUES + ); + return; + } + + if (values.size() < MIN_ADD_ALL_SIZE) { + return; + } + + // The added values are not enum values, but some other value. + // + // Suggesting to use `addAll` instead of multiple `add` invocations is pedantic, + // when the values can not be refactored into a constant: + // + // ``` + // list.addAll(List.of( + // var1, + // var2, + // var3 + // )); + // ``` + // is not much better than + // ``` + // list.add(var1); + // list.add(var2); + // list.add(var3); + // ``` + // + // Therefore, we only suggest to use `addAll` if the values can be refactored into a constant. + + for (CtExpression value: values) { + if (!ExpressionUtil.isConstantExpressionOr(value, e -> false)) { + return; + } + } + + addLocalProblem( + values.get(0).getParent(CtStatement.class), + new LocalizedMessage( + "common-reimplementation", + Map.of( + "suggestion", "private static final List<%s> SOME_GOOD_NAME = List.of(%s); /* ... */ %s.addAll(SOME_GOOD_NAME)".formatted( + ExpressionUtil.getExpressionType(values.get(0)), + values.stream() + .map(CtElement::toString) + .collect(Collectors.joining(", ")), + ctVariable.getSimpleName() + ) + ) + ), + ProblemType.SEQUENTIAL_ADD_ALL + ); + } + + @Override + protected void check(StaticAnalysis staticAnalysis) { + staticAnalysis.processWith(new AbstractProcessor>() { + @Override + public void process(CtBlock ctBlock) { + if (ctBlock.isImplicit() || !ctBlock.getPosition().isValidPosition() || ctBlock.getStatements().size() < MIN_ADD_ALL_SIZE) { + return; + } + + CtVariableReference collection = null; + List> addedValues = new ArrayList<>(); + + // We want to find a sequence of add invocations in the block like this: + // + // ``` + // list.add("a"); + // list.add("b"); + // list.add("c"); + // list.add("d"); + // ... + // ``` + for (CtStatement ctStatement : StatementUtil.getEffectiveStatements(ctBlock)) { + AddInvocation addInvocation = AddInvocation.of(ctStatement).orElse(null); + // the current statement might not be an add invocation + if (addInvocation == null) { + // This if handles the case where there is code after the sequence of add invocations: + // + // ``` + // list.add("a"); + // list.add("b"); + // list.add("c"); + // list.add("d"); + // + // System.out.println("Hello, World!"); // <- extra code + // ``` + if (addedValues.size() > 1 && collection != null) { + reportProblem(collection, addedValues); + } + + // otherwise reset the state and start over + collection = null; + continue; + } + + // if the collection is not yet known, we have found the first add invocation + if (collection == null) { + collection = addInvocation.collection(); + addedValues.clear(); + } + + // we might encounter an add invocation for a different collection + if (!collection.equals(addInvocation.collection())) { + // if we have enough values, report the problem + if (addedValues.size() > 1) { + reportProblem(collection, addedValues); + } + + // if there are multiple collections, just invalidate the state + collection = null; + continue; + } + + addedValues.add(addInvocation.value()); + } + + // this happens when the last statement in the block is an add invocation + if (addedValues.size() > 1 && collection != null) { + reportProblem(collection, addedValues); + } + } + }); + } +} diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/check/api/SimplifyArraysFill.java b/autograder-core/src/main/java/de/firemage/autograder/core/check/api/SimplifyArraysFill.java index 94304e9b..862f0267 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/check/api/SimplifyArraysFill.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/check/api/SimplifyArraysFill.java @@ -3,9 +3,10 @@ import de.firemage.autograder.core.LocalizedMessage; import de.firemage.autograder.core.ProblemType; import de.firemage.autograder.core.check.ExecutableCheck; +import de.firemage.autograder.core.integrated.ExpressionUtil; import de.firemage.autograder.core.integrated.IntegratedCheck; -import de.firemage.autograder.core.integrated.SpoonUtil; import de.firemage.autograder.core.integrated.StaticAnalysis; +import de.firemage.autograder.core.integrated.TypeUtil; import spoon.processing.AbstractProcessor; import spoon.reflect.code.CtExpression; import spoon.reflect.code.CtFieldAccess; @@ -29,14 +30,14 @@ public void process(CtInvocation ctInvocation) { } if (!(ctInvocation.getTarget() instanceof CtTypeAccess ctTypeAccess) - || !SpoonUtil.isTypeEqualTo(ctTypeAccess.getAccessedType(), java.util.Arrays.class) + || !TypeUtil.isTypeEqualTo(ctTypeAccess.getAccessedType(), java.util.Arrays.class) || !ctInvocation.getExecutable().getSimpleName().equals("fill") || ctInvocation.getArguments().size() != 4) { return; } List> args = ctInvocation.getArguments(); - if (SpoonUtil.resolveConstant(args.get(1)) instanceof CtLiteral ctLiteral + if (ExpressionUtil.resolveConstant(args.get(1)) instanceof CtLiteral ctLiteral && ctLiteral.getValue() instanceof Integer number && number == 0 && args.get(2) instanceof CtFieldAccess ctFieldAccess diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/check/api/StringRepeat.java b/autograder-core/src/main/java/de/firemage/autograder/core/check/api/StringRepeat.java index 12218cd9..8fc3f603 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/check/api/StringRepeat.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/check/api/StringRepeat.java @@ -4,10 +4,12 @@ import de.firemage.autograder.core.ProblemType; import de.firemage.autograder.core.check.ExecutableCheck; +import de.firemage.autograder.core.integrated.ExpressionUtil; import de.firemage.autograder.core.integrated.ForLoopRange; import de.firemage.autograder.core.integrated.IntegratedCheck; -import de.firemage.autograder.core.integrated.SpoonUtil; +import de.firemage.autograder.core.integrated.StatementUtil; import de.firemage.autograder.core.integrated.StaticAnalysis; +import de.firemage.autograder.core.integrated.TypeUtil; import spoon.processing.AbstractProcessor; import spoon.reflect.code.BinaryOperatorKind; import spoon.reflect.code.CtExpression; @@ -25,7 +27,7 @@ public class StringRepeat extends IntegratedCheck { private void checkStringRepeat(CtFor ctFor) { ForLoopRange forLoopRange = ForLoopRange.fromCtFor(ctFor).orElse(null); - List statements = SpoonUtil.getEffectiveStatements(ctFor.getBody()); + List statements = StatementUtil.getEffectiveStatements(ctFor.getBody()); if (statements.size() != 1 || forLoopRange == null) { return; } @@ -34,11 +36,11 @@ private void checkStringRepeat(CtFor ctFor) { if (statements.get(0) instanceof CtOperatorAssignment ctAssignment && ctAssignment.getKind() == BinaryOperatorKind.PLUS) { CtExpression lhs = ctAssignment.getAssigned(); - if (!SpoonUtil.isTypeEqualTo(lhs.getType(), String.class)) { + if (!TypeUtil.isTypeEqualTo(lhs.getType(), String.class)) { return; } - CtExpression rhs = SpoonUtil.resolveCtExpression(ctAssignment.getAssignment()); + CtExpression rhs = ExpressionUtil.resolveCtExpression(ctAssignment.getAssignment()); // return if the for loop uses the loop variable (would not be a simple repetition) if (!ctAssignment.getElements(new VariableAccessFilter<>(forLoopRange.loopVariable())).isEmpty()) { return; diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/check/api/UseArrayCopy.java b/autograder-core/src/main/java/de/firemage/autograder/core/check/api/UseArrayCopy.java index ba294d68..ed9e772b 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/check/api/UseArrayCopy.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/check/api/UseArrayCopy.java @@ -6,7 +6,7 @@ import de.firemage.autograder.core.integrated.ForLoopRange; import de.firemage.autograder.core.integrated.IntegratedCheck; -import de.firemage.autograder.core.integrated.SpoonUtil; +import de.firemage.autograder.core.integrated.StatementUtil; import de.firemage.autograder.core.integrated.StaticAnalysis; import spoon.processing.AbstractProcessor; import spoon.reflect.code.CtArrayAccess; @@ -25,7 +25,7 @@ public class UseArrayCopy extends IntegratedCheck { private void checkArrayCopy(CtFor ctFor) { ForLoopRange forLoopRange = ForLoopRange.fromCtFor(ctFor).orElse(null); - List statements = SpoonUtil.getEffectiveStatements(ctFor.getBody()); + List statements = StatementUtil.getEffectiveStatements(ctFor.getBody()); if (statements.size() != 1 || forLoopRange == null) { return; } diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/check/api/UseArraysFill.java b/autograder-core/src/main/java/de/firemage/autograder/core/check/api/UseArraysFill.java index 0566a57f..bddeb1c2 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/check/api/UseArraysFill.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/check/api/UseArraysFill.java @@ -5,8 +5,10 @@ import de.firemage.autograder.core.check.ExecutableCheck; import de.firemage.autograder.core.integrated.ForLoopRange; import de.firemage.autograder.core.integrated.IntegratedCheck; -import de.firemage.autograder.core.integrated.SpoonUtil; +import de.firemage.autograder.core.integrated.VariableUtil; +import de.firemage.autograder.core.integrated.StatementUtil; import de.firemage.autograder.core.integrated.StaticAnalysis; +import de.firemage.autograder.core.integrated.TypeUtil; import de.firemage.autograder.core.integrated.UsesFinder; import spoon.processing.AbstractProcessor; import spoon.reflect.code.CtArrayWrite; @@ -30,7 +32,7 @@ public class UseArraysFill extends IntegratedCheck { private void checkArraysFill(CtFor ctFor) { ForLoopRange forLoopRange = ForLoopRange.fromCtFor(ctFor).orElse(null); - List statements = SpoonUtil.getEffectiveStatements(ctFor.getBody()); + List statements = StatementUtil.getEffectiveStatements(ctFor.getBody()); if (statements.size() != 1 || forLoopRange == null @@ -41,7 +43,7 @@ private void checkArraysFill(CtFor ctFor) { return; } - CtVariable loopVariable = (CtVariable) SpoonUtil.getReferenceDeclaration(forLoopRange.loopVariable()); + CtVariable loopVariable = (CtVariable) VariableUtil.getReferenceDeclaration(forLoopRange.loopVariable()); // return if the for loop uses the loop variable (would not be a simple repetition) if (UsesFinder.variableUses(loopVariable).nestedIn(ctAssignment.getAssignment()).hasAny()) { return; @@ -53,7 +55,7 @@ private void checkArraysFill(CtFor ctFor) { } CtExpression rhs = ctAssignment.getAssignment(); - if (!SpoonUtil.isImmutable(rhs.getType())) { + if (!TypeUtil.isImmutable(rhs.getType())) { return; } diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/check/api/UseEntrySet.java b/autograder-core/src/main/java/de/firemage/autograder/core/check/api/UseEntrySet.java index 52a37068..5ca20226 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/check/api/UseEntrySet.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/check/api/UseEntrySet.java @@ -3,9 +3,10 @@ import de.firemage.autograder.core.LocalizedMessage; import de.firemage.autograder.core.ProblemType; import de.firemage.autograder.core.check.ExecutableCheck; +import de.firemage.autograder.core.integrated.ExpressionUtil; import de.firemage.autograder.core.integrated.IntegratedCheck; -import de.firemage.autograder.core.integrated.SpoonUtil; import de.firemage.autograder.core.integrated.StaticAnalysis; +import de.firemage.autograder.core.integrated.TypeUtil; import spoon.processing.AbstractProcessor; import spoon.reflect.code.CtForEach; import spoon.reflect.code.CtInvocation; @@ -22,7 +23,7 @@ public class UseEntrySet extends IntegratedCheck { private static boolean hasInvokedKeySet(CtInvocation ctInvocation) { return ctInvocation.getTarget() != null && ctInvocation.getExecutable() != null - && SpoonUtil.isSubtypeOf(ctInvocation.getTarget().getType(), java.util.Map.class) + && TypeUtil.isSubtypeOf(ctInvocation.getTarget().getType(), java.util.Map.class) && ctInvocation.getExecutable().getSimpleName().equals("keySet"); } @@ -33,7 +34,7 @@ protected void check(StaticAnalysis staticAnalysis) { public void process(CtForEach ctForEach) { if (ctForEach.isImplicit() || !ctForEach.getPosition().isValidPosition() - || !(SpoonUtil.resolveCtExpression(ctForEach.getExpression()) instanceof CtInvocation ctInvocation) + || !(ExpressionUtil.resolveCtExpression(ctForEach.getExpression()) instanceof CtInvocation ctInvocation) || !hasInvokedKeySet(ctInvocation) || !ctForEach.getExpression().getPosition().isValidPosition()) { return; diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/check/api/UseEnumValues.java b/autograder-core/src/main/java/de/firemage/autograder/core/check/api/UseEnumValues.java index beb3a2cb..a732449f 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/check/api/UseEnumValues.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/check/api/UseEnumValues.java @@ -3,8 +3,10 @@ import de.firemage.autograder.core.LocalizedMessage; import de.firemage.autograder.core.ProblemType; import de.firemage.autograder.core.check.ExecutableCheck; +import de.firemage.autograder.core.integrated.ExpressionUtil; import de.firemage.autograder.core.integrated.IntegratedCheck; -import de.firemage.autograder.core.integrated.SpoonUtil; +import de.firemage.autograder.core.integrated.TypeUtil; +import de.firemage.autograder.core.integrated.VariableUtil; import de.firemage.autograder.core.integrated.StaticAnalysis; import spoon.processing.AbstractProcessor; @@ -25,15 +27,8 @@ import java.util.function.UnaryOperator; import java.util.stream.Stream; -@ExecutableCheck(reportedProblems = {ProblemType.COMMON_REIMPLEMENTATION_ADD_ENUM_VALUES}) +@ExecutableCheck(reportedProblems = {ProblemType.USE_ENUM_VALUES}) public class UseEnumValues extends IntegratedCheck { - - private static boolean isOrderedCollection(CtTypeReference ctTypeReference) { - return Stream.of(java.util.List.class) - .map(ctClass -> ctTypeReference.getFactory().createCtTypeReference(ctClass)) - .anyMatch(ctTypeReference::isSubtypeOf); - } - public static boolean checkEnumValues( CtEnum ctEnum, boolean isOrdered, @@ -65,18 +60,17 @@ public static Optional of(CtExpression ctExpression) { return Optional.empty(); } - // check if the expression is an enum type - if (!ctExpression.getType().isEnum() + if (ctExpression.getType().isEnum() // that it accesses a variant of the enum - || !(ctExpression instanceof CtFieldRead ctFieldRead) + && ctExpression instanceof CtFieldRead ctFieldRead // that the field is a variant of the enum - || !(ctFieldRead.getVariable().getDeclaration() instanceof CtEnumValue ctEnumValue) + && ctFieldRead.getVariable().getDeclaration() instanceof CtEnumValue ctEnumValue // that the field is in an enum - || !(ctEnumValue.getDeclaringType() instanceof CtEnum ctEnum)) { - return Optional.empty(); + && ctEnumValue.getDeclaringType() instanceof CtEnum ctEnum) { + return Optional.of(new CtEnumFieldRead(ctEnum, ctEnumValue)); } - return Optional.of(new CtEnumFieldRead(ctEnum, ctEnumValue)); + return Optional.empty(); } } @@ -113,7 +107,7 @@ private void checkListingEnumValues( "suggestion", suggestion.apply("%s.values()".formatted(ctEnum.getSimpleName())) ) ), - ProblemType.COMMON_REIMPLEMENTATION_ADD_ENUM_VALUES + ProblemType.USE_ENUM_VALUES ); } } @@ -125,7 +119,7 @@ protected void check(StaticAnalysis staticAnalysis) { staticAnalysis.processWith(new AbstractProcessor>() { @Override public void process(CtField ctField) { - if (!SpoonUtil.isEffectivelyFinal(ctField)) { + if (!VariableUtil.isEffectivelyFinal(ctField)) { return; } @@ -143,8 +137,8 @@ public void process(CtField ctField) { ); } else { checkListingEnumValues( - isOrderedCollection(ctExpression.getType()), - SpoonUtil.getElementsOfExpression(ctExpression), + TypeUtil.isOrderedCollection(ctExpression.getType()), + ExpressionUtil.getElementsOfExpression(ctExpression), suggestion -> "%s.of(%s)".formatted(ctExpression.getType().getSimpleName(), suggestion), ctExpression ); diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/check/api/UseFormatString.java b/autograder-core/src/main/java/de/firemage/autograder/core/check/api/UseFormatString.java index 164bf4e7..feb5f02b 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/check/api/UseFormatString.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/check/api/UseFormatString.java @@ -3,9 +3,12 @@ import de.firemage.autograder.core.LocalizedMessage; import de.firemage.autograder.core.ProblemType; import de.firemage.autograder.core.check.ExecutableCheck; +import de.firemage.autograder.core.integrated.ExpressionUtil; +import de.firemage.autograder.core.integrated.FactoryUtil; import de.firemage.autograder.core.integrated.IntegratedCheck; -import de.firemage.autograder.core.integrated.SpoonUtil; import de.firemage.autograder.core.integrated.StaticAnalysis; +import de.firemage.autograder.core.integrated.MethodUtil; +import de.firemage.autograder.core.integrated.TypeUtil; import spoon.processing.AbstractProcessor; import spoon.reflect.code.BinaryOperatorKind; import spoon.reflect.code.CtBinaryOperator; @@ -83,9 +86,9 @@ private String buildFormattedString(Iterable> ctExpres // true if the formatString has a %n placeholder boolean hasInlineNewline = false; for (CtExpression ctExpression : ctExpressions) { - if (SpoonUtil.resolveConstant(ctExpression) instanceof CtLiteral literal + if (ExpressionUtil.resolveConstant(ctExpression) instanceof CtLiteral literal && literal.getValue() != null - && SpoonUtil.isTypeEqualTo(literal.getType(), java.lang.String.class)) { + && TypeUtil.isTypeEqualTo(literal.getType(), java.lang.String.class)) { ctExpression = literal; } @@ -138,18 +141,18 @@ private CtExpression resolveExpression(CtExpression ctExpression) { if (ctExpression instanceof CtInvocation ctInvocation && ctInvocation.getTarget() instanceof CtTypeAccess ctTypeAccess // ensure the method is called on java.lang.System - && SpoonUtil.isTypeEqualTo(ctTypeAccess.getAccessedType(), java.lang.System.class) - && SpoonUtil.isSignatureEqualTo( + && TypeUtil.isTypeEqualTo(ctTypeAccess.getAccessedType(), java.lang.System.class) + && MethodUtil.isSignatureEqualTo( ctInvocation.getExecutable(), typeFactory.stringType(), "lineSeparator" )) { - return SpoonUtil.makeLiteral(typeFactory.stringType(), "\n"); + return FactoryUtil.makeLiteral(typeFactory.stringType(), "\n"); } if (ctExpression instanceof CtLiteral ctLiteral - && SpoonUtil.areLiteralsEqual(ctLiteral, SpoonUtil.makeLiteral(typeFactory.characterPrimitiveType(), '\n'))) { - return SpoonUtil.makeLiteral(typeFactory.stringType(), "\n"); + && ExpressionUtil.areLiteralsEqual(ctLiteral, FactoryUtil.makeLiteral(typeFactory.characterPrimitiveType(), '\n'))) { + return FactoryUtil.makeLiteral(typeFactory.stringType(), "\n"); } return ctExpression; @@ -196,13 +199,13 @@ private void checkCtBinaryOperator(CtBinaryOperator ctBinaryOperator) { // only visit binary operators that evaluate to a String // (should be guaranteed by the visitor) -> seems to not be guaranteed; replacing the throw by a return for now - if (!SpoonUtil.isString(ctBinaryOperator.getType())) { + if (!TypeUtil.isString(ctBinaryOperator.getType())) { return; } List> formatArgs = this.getFormatArgs(ctBinaryOperator); - int numberOfLiterals = (int) formatArgs.stream().filter(ctExpression -> SpoonUtil.resolveConstant(ctExpression) instanceof CtLiteral literal && literal.getValue() != null).count(); + int numberOfLiterals = (int) formatArgs.stream().filter(ctExpression -> ExpressionUtil.resolveConstant(ctExpression) instanceof CtLiteral literal && literal.getValue() != null).count(); if (numberOfLiterals < MIN_NUMBER_LITERALS) { return; } diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/check/api/UseModuloOperator.java b/autograder-core/src/main/java/de/firemage/autograder/core/check/api/UseModuloOperator.java index 6c185820..8c61b893 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/check/api/UseModuloOperator.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/check/api/UseModuloOperator.java @@ -3,8 +3,9 @@ import de.firemage.autograder.core.LocalizedMessage; import de.firemage.autograder.core.ProblemType; import de.firemage.autograder.core.check.ExecutableCheck; +import de.firemage.autograder.core.integrated.ExpressionUtil; import de.firemage.autograder.core.integrated.IntegratedCheck; -import de.firemage.autograder.core.integrated.SpoonUtil; +import de.firemage.autograder.core.integrated.StatementUtil; import de.firemage.autograder.core.integrated.StaticAnalysis; import spoon.processing.AbstractProcessor; import spoon.reflect.code.BinaryOperatorKind; @@ -33,7 +34,7 @@ public class UseModuloOperator extends IntegratedCheck { ); private void checkModulo(CtIf ctIf) { - List thenBlock = SpoonUtil.getEffectiveStatements(ctIf.getThenStatement()); + List thenBlock = StatementUtil.getEffectiveStatements(ctIf.getThenStatement()); if (ctIf.getElseStatement() != null || thenBlock.size() != 1 || !(thenBlock.get(0) instanceof CtAssignment thenAssignment) @@ -44,7 +45,7 @@ private void checkModulo(CtIf ctIf) { } // must assign a value of 0 - if (!(SpoonUtil.resolveCtExpression(thenAssignment.getAssignment()) instanceof CtLiteral ctLiteral) + if (!(ExpressionUtil.resolveCtExpression(thenAssignment.getAssignment()) instanceof CtLiteral ctLiteral) || !(ctLiteral.getValue() instanceof Integer integer) || integer != 0) { return; @@ -52,7 +53,7 @@ private void checkModulo(CtIf ctIf) { CtVariableReference assignedVariable = ctVariableWrite.getVariable(); - CtBinaryOperator condition = SpoonUtil.normalizeBy( + CtBinaryOperator condition = ExpressionUtil.normalizeBy( (left, right) -> right instanceof CtVariableAccess ctVariableAccess && ctVariableAccess.getVariable().equals(assignedVariable), ctBinaryOperator ); @@ -74,7 +75,7 @@ private void checkModulo(CtIf ctIf) { // for boxed types, one could check if the value is null, // for which the suggestion `a %= null` would not make sense - if (SpoonUtil.isNullLiteral(checkedValue)) { + if (ExpressionUtil.isNullLiteral(checkedValue)) { return; } diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/check/api/UseStringFormatted.java b/autograder-core/src/main/java/de/firemage/autograder/core/check/api/UseStringFormatted.java index 34f918de..c11e8742 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/check/api/UseStringFormatted.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/check/api/UseStringFormatted.java @@ -3,9 +3,10 @@ import de.firemage.autograder.core.LocalizedMessage; import de.firemage.autograder.core.ProblemType; import de.firemage.autograder.core.check.ExecutableCheck; +import de.firemage.autograder.core.integrated.ExpressionUtil; import de.firemage.autograder.core.integrated.IntegratedCheck; -import de.firemage.autograder.core.integrated.SpoonUtil; import de.firemage.autograder.core.integrated.StaticAnalysis; +import de.firemage.autograder.core.integrated.TypeUtil; import spoon.processing.AbstractProcessor; import spoon.reflect.code.CtExpression; import spoon.reflect.code.CtInvocation; @@ -21,11 +22,11 @@ public class UseStringFormatted extends IntegratedCheck { private void checkCtInvocation(CtInvocation ctInvocation) { boolean hasInvokedStringFormat = ctInvocation.getTarget() instanceof CtTypeAccess ctTypeAccess // ensure the method is called on java.lang.String - && SpoonUtil.isTypeEqualTo(ctTypeAccess.getAccessedType(), java.lang.String.class) + && TypeUtil.isTypeEqualTo(ctTypeAccess.getAccessedType(), java.lang.String.class) && ctInvocation.getExecutable().getSimpleName().equals("format") && !ctInvocation.getArguments().isEmpty() // ensure the first argument is a string (this ignores String.format(Locale, String, Object...)) - && SpoonUtil.isTypeEqualTo(ctInvocation.getArguments().get(0).getType(), java.lang.String.class); + && TypeUtil.isTypeEqualTo(ctInvocation.getArguments().get(0).getType(), java.lang.String.class); if (!hasInvokedStringFormat) { return; @@ -38,7 +39,7 @@ private void checkCtInvocation(CtInvocation ctInvocation) { CtExpression format = args.remove(0); // skip if the format string is not a string literal (e.g. a complex concatenation) - if (SpoonUtil.tryGetStringLiteral(SpoonUtil.resolveConstant(format)).isEmpty()) { + if (ExpressionUtil.tryGetStringLiteral(ExpressionUtil.resolveConstant(format)).isEmpty()) { return; } diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/check/api/UseSubList.java b/autograder-core/src/main/java/de/firemage/autograder/core/check/api/UseSubList.java index 972a9e76..cedb82c0 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/check/api/UseSubList.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/check/api/UseSubList.java @@ -5,9 +5,9 @@ import de.firemage.autograder.core.check.ExecutableCheck; import de.firemage.autograder.core.check.general.ForToForEachLoop; +import de.firemage.autograder.core.integrated.ExpressionUtil; import de.firemage.autograder.core.integrated.ForLoopRange; import de.firemage.autograder.core.integrated.IntegratedCheck; -import de.firemage.autograder.core.integrated.SpoonUtil; import de.firemage.autograder.core.integrated.StaticAnalysis; import spoon.processing.AbstractProcessor; import spoon.reflect.code.CtFor; @@ -45,7 +45,7 @@ private void checkSubList(CtFor ctFor) { } // check if the loop iterates over the whole list (then it is covered by the foreach loop check) - if (SpoonUtil.resolveConstant(forLoopRange.start()) instanceof CtLiteral ctLiteral + if (ExpressionUtil.resolveConstant(forLoopRange.start()) instanceof CtLiteral ctLiteral && ctLiteral.getValue() == 0 && ForToForEachLoop.findIterable(forLoopRange).isPresent()) { return; diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/check/comment/FieldJavadocCheck.java b/autograder-core/src/main/java/de/firemage/autograder/core/check/comment/FieldJavadocCheck.java index 9af70eb8..f8f9b6e0 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/check/comment/FieldJavadocCheck.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/check/comment/FieldJavadocCheck.java @@ -3,8 +3,8 @@ import de.firemage.autograder.core.LocalizedMessage; import de.firemage.autograder.core.ProblemType; import de.firemage.autograder.core.check.ExecutableCheck; +import de.firemage.autograder.core.integrated.ElementUtil; import de.firemage.autograder.core.integrated.IntegratedCheck; -import de.firemage.autograder.core.integrated.SpoonUtil; import de.firemage.autograder.core.integrated.StaticAnalysis; import spoon.processing.AbstractProcessor; import spoon.reflect.code.CtJavaDoc; @@ -32,7 +32,7 @@ public void process(CtField field) { return; } - Optional javadoc = SpoonUtil.getJavadoc(field); + Optional javadoc = ElementUtil.getJavadoc(field); if (javadoc.isEmpty()) { return; } diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/check/comment/JavadocStubCheck.java b/autograder-core/src/main/java/de/firemage/autograder/core/check/comment/JavadocStubCheck.java index 10c3ddca..873e61df 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/check/comment/JavadocStubCheck.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/check/comment/JavadocStubCheck.java @@ -4,7 +4,7 @@ import de.firemage.autograder.core.ProblemType; import de.firemage.autograder.core.check.ExecutableCheck; import de.firemage.autograder.core.integrated.IntegratedCheck; -import de.firemage.autograder.core.integrated.SpoonUtil; +import de.firemage.autograder.core.integrated.MethodUtil; import de.firemage.autograder.core.integrated.StaticAnalysis; import spoon.processing.AbstractProcessor; import spoon.reflect.code.CtJavaDoc; @@ -34,7 +34,7 @@ protected void check(StaticAnalysis staticAnalysis) { public void process(CtJavaDoc javadoc) { if (allowGettersSettersWithEmptyDescription && javadoc.getParent() instanceof CtMethod method - && (SpoonUtil.isGetter(method) || SpoonUtil.isSetter(method))) { + && (MethodUtil.isGetter(method) || MethodUtil.isSetter(method))) { // Setters and Getters are okay } else if (isDefaultValueDescription(javadoc.getContent())) { addLocalProblem(javadoc, new LocalizedMessage("javadoc-stub-exp-desc"), diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/check/comment/MethodJavadocCheck.java b/autograder-core/src/main/java/de/firemage/autograder/core/check/comment/MethodJavadocCheck.java index 5f7a8fd6..b128354c 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/check/comment/MethodJavadocCheck.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/check/comment/MethodJavadocCheck.java @@ -3,8 +3,8 @@ import de.firemage.autograder.core.LocalizedMessage; import de.firemage.autograder.core.ProblemType; import de.firemage.autograder.core.check.ExecutableCheck; +import de.firemage.autograder.core.integrated.ElementUtil; import de.firemage.autograder.core.integrated.IntegratedCheck; -import de.firemage.autograder.core.integrated.SpoonUtil; import de.firemage.autograder.core.integrated.StaticAnalysis; import spoon.processing.AbstractProcessor; import spoon.reflect.code.CtJavaDoc; @@ -41,7 +41,7 @@ public void process(CtMethod method) { return; } - Optional javadoc = SpoonUtil.getJavadoc(method); + Optional javadoc = ElementUtil.getJavadoc(method); if (javadoc.isEmpty()) { return; } diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/check/comment/ThrowsJavadocCheck.java b/autograder-core/src/main/java/de/firemage/autograder/core/check/comment/ThrowsJavadocCheck.java index 0d885560..42715b67 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/check/comment/ThrowsJavadocCheck.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/check/comment/ThrowsJavadocCheck.java @@ -3,8 +3,8 @@ import de.firemage.autograder.core.LocalizedMessage; import de.firemage.autograder.core.ProblemType; import de.firemage.autograder.core.check.ExecutableCheck; +import de.firemage.autograder.core.integrated.ElementUtil; import de.firemage.autograder.core.integrated.IntegratedCheck; -import de.firemage.autograder.core.integrated.SpoonUtil; import de.firemage.autograder.core.integrated.StaticAnalysis; import spoon.reflect.code.CtCase; import spoon.reflect.code.CtConstructorCall; @@ -25,7 +25,7 @@ @ExecutableCheck(reportedProblems = {ProblemType.JAVADOC_UNDOCUMENTED_THROWS}) public class ThrowsJavadocCheck extends IntegratedCheck { private void checkCtExecutable(CtExecutable ctExecutable) { - Optional doc = SpoonUtil.getJavadoc(ctExecutable); + Optional doc = ElementUtil.getJavadoc(ctExecutable); if (doc.isEmpty()) { return; } diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/check/comment/TypeJavadocCheck.java b/autograder-core/src/main/java/de/firemage/autograder/core/check/comment/TypeJavadocCheck.java index 6f6ab3c0..c6705f60 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/check/comment/TypeJavadocCheck.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/check/comment/TypeJavadocCheck.java @@ -3,8 +3,8 @@ import de.firemage.autograder.core.LocalizedMessage; import de.firemage.autograder.core.ProblemType; import de.firemage.autograder.core.check.ExecutableCheck; +import de.firemage.autograder.core.integrated.ElementUtil; import de.firemage.autograder.core.integrated.IntegratedCheck; -import de.firemage.autograder.core.integrated.SpoonUtil; import de.firemage.autograder.core.integrated.StaticAnalysis; import spoon.processing.AbstractProcessor; import spoon.reflect.code.CtJavaDoc; @@ -51,7 +51,7 @@ protected void check(StaticAnalysis staticAnalysis) { staticAnalysis.processWith(new AbstractProcessor>() { @Override public void process(CtType type) { - Optional javadoc = SpoonUtil.getJavadoc(type); + Optional javadoc = ElementUtil.getJavadoc(type); if (javadoc.isEmpty()) { return; } diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/check/comment/UnnecessaryComment.java b/autograder-core/src/main/java/de/firemage/autograder/core/check/comment/UnnecessaryComment.java index 997d82ce..27e74d7c 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/check/comment/UnnecessaryComment.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/check/comment/UnnecessaryComment.java @@ -4,7 +4,7 @@ import de.firemage.autograder.core.ProblemType; import de.firemage.autograder.core.check.ExecutableCheck; import de.firemage.autograder.core.integrated.IntegratedCheck; -import de.firemage.autograder.core.integrated.SpoonUtil; +import de.firemage.autograder.core.integrated.StatementUtil; import de.firemage.autograder.core.integrated.StaticAnalysis; import spoon.processing.AbstractProcessor; import spoon.reflect.code.CtComment; @@ -69,7 +69,7 @@ public void process(CtElement element) { // // Here the comments without an attached CtElement are processed: if (element instanceof CtComment ctComment && isStandaloneComment(ctComment) && !visitedComments.contains(ctComment)) { - List followingComments = SpoonUtil.getNextStatements(ctComment) + List followingComments = StatementUtil.getNextStatements(ctComment) .stream() .takeWhile(CtComment.class::isInstance) .map(CtComment.class::cast) diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/check/complexity/ChainedIfCheck.java b/autograder-core/src/main/java/de/firemage/autograder/core/check/complexity/ChainedIfCheck.java index 490c041a..75fdb9b0 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/check/complexity/ChainedIfCheck.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/check/complexity/ChainedIfCheck.java @@ -3,8 +3,9 @@ import de.firemage.autograder.core.LocalizedMessage; import de.firemage.autograder.core.ProblemType; import de.firemage.autograder.core.check.ExecutableCheck; +import de.firemage.autograder.core.integrated.FactoryUtil; import de.firemage.autograder.core.integrated.IntegratedCheck; -import de.firemage.autograder.core.integrated.SpoonUtil; +import de.firemage.autograder.core.integrated.StatementUtil; import de.firemage.autograder.core.integrated.StaticAnalysis; import spoon.processing.AbstractProcessor; import spoon.reflect.code.CtBlock; @@ -27,17 +28,17 @@ public void process(CtIf ctIf) { } // check if the if-statement has a nested if: - List thenStatements = SpoonUtil.getEffectiveStatements(ctIf.getThenStatement()); + List thenStatements = StatementUtil.getEffectiveStatements(ctIf.getThenStatement()); if (thenStatements.size() == 1 && thenStatements.get(0) instanceof CtIf nestedIf && (nestedIf.getElseStatement() == null - || SpoonUtil.getEffectiveStatements(nestedIf.getElseStatement()).isEmpty())) { + || StatementUtil.getEffectiveStatements(nestedIf.getElseStatement()).isEmpty())) { addLocalProblem( ctIf.getCondition(), new LocalizedMessage( "merge-nested-if", Map.of( - "suggestion", SpoonUtil.createBinaryOperator( + "suggestion", FactoryUtil.createBinaryOperator( ctIf.getCondition(), nestedIf.getCondition(), spoon.reflect.code.BinaryOperatorKind.AND @@ -53,7 +54,7 @@ public void process(CtIf ctIf) { return; } - List statements = SpoonUtil.getEffectiveStatements(ctBlock); + List statements = StatementUtil.getEffectiveStatements(ctBlock); if (statements.size() != 1) { return; } diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/check/complexity/RedundantAssignment.java b/autograder-core/src/main/java/de/firemage/autograder/core/check/complexity/RedundantAssignment.java index 0bdb8ead..ec74dae1 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/check/complexity/RedundantAssignment.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/check/complexity/RedundantAssignment.java @@ -5,7 +5,7 @@ import de.firemage.autograder.core.check.ExecutableCheck; import de.firemage.autograder.core.check.unnecessary.UnusedCodeElementCheck; import de.firemage.autograder.core.integrated.IntegratedCheck; -import de.firemage.autograder.core.integrated.SpoonUtil; +import de.firemage.autograder.core.integrated.StatementUtil; import de.firemage.autograder.core.integrated.StaticAnalysis; import de.firemage.autograder.core.integrated.UsesFinder; import spoon.processing.AbstractProcessor; @@ -42,7 +42,7 @@ public void process(CtAssignment ctAssignment) { return; } - List followingStatements = SpoonUtil.getNextStatements(ctAssignment); + List followingStatements = StatementUtil.getNextStatements(ctAssignment); CtLocalVariable ctLocalVariable = ctLocalVariableReference.getDeclaration(); diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/check/complexity/RedundantBooleanEqual.java b/autograder-core/src/main/java/de/firemage/autograder/core/check/complexity/RedundantBooleanEqual.java index cd125f76..ac09ffdf 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/check/complexity/RedundantBooleanEqual.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/check/complexity/RedundantBooleanEqual.java @@ -3,8 +3,8 @@ import de.firemage.autograder.core.LocalizedMessage; import de.firemage.autograder.core.ProblemType; import de.firemage.autograder.core.check.ExecutableCheck; +import de.firemage.autograder.core.integrated.ExpressionUtil; import de.firemage.autograder.core.integrated.IntegratedCheck; -import de.firemage.autograder.core.integrated.SpoonUtil; import de.firemage.autograder.core.integrated.StaticAnalysis; import spoon.processing.AbstractProcessor; import spoon.reflect.code.BinaryOperatorKind; @@ -29,7 +29,7 @@ private void reportProblem(CtBinaryOperator ctBinaryOperator, boolean literal CtExpression suggestion = otherSide; if (!realLiteral) { - suggestion = SpoonUtil.negate(otherSide); + suggestion = ExpressionUtil.negate(otherSide); } addLocalProblem( @@ -53,8 +53,8 @@ public void process(CtBinaryOperator ctBinaryOperator) { if (ctBinaryOperator.isImplicit() || !ctBinaryOperator.getPosition().isValidPosition() || !OPERATORS.contains(ctBinaryOperator.getKind()) - || !SpoonUtil.isBoolean(ctBinaryOperator.getLeftHandOperand()) - || !SpoonUtil.isBoolean(ctBinaryOperator.getRightHandOperand())) { + || !ExpressionUtil.isBoolean(ctBinaryOperator.getLeftHandOperand()) + || !ExpressionUtil.isBoolean(ctBinaryOperator.getRightHandOperand())) { return; } @@ -62,11 +62,11 @@ public void process(CtBinaryOperator ctBinaryOperator) { CtExpression right = ctBinaryOperator.getRightHandOperand(); // the lhs resolves to a literal boolean - SpoonUtil.tryGetBooleanLiteral(left) + ExpressionUtil.tryGetBooleanLiteral(left) .ifPresentOrElse( literal -> reportProblem(ctBinaryOperator, literal, right), // if the lhs is not a literal boolean, check if the rhs is - () -> SpoonUtil.tryGetBooleanLiteral(right) + () -> ExpressionUtil.tryGetBooleanLiteral(right) .ifPresent(literal -> reportProblem(ctBinaryOperator, literal, left)) ); } diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/check/complexity/RedundantCatch.java b/autograder-core/src/main/java/de/firemage/autograder/core/check/complexity/RedundantCatch.java index ae55a869..0eefb46b 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/check/complexity/RedundantCatch.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/check/complexity/RedundantCatch.java @@ -4,7 +4,7 @@ import de.firemage.autograder.core.ProblemType; import de.firemage.autograder.core.check.ExecutableCheck; import de.firemage.autograder.core.integrated.IntegratedCheck; -import de.firemage.autograder.core.integrated.SpoonUtil; +import de.firemage.autograder.core.integrated.StatementUtil; import de.firemage.autograder.core.integrated.StaticAnalysis; import de.firemage.autograder.core.integrated.effects.Effect; import de.firemage.autograder.core.integrated.effects.TerminalEffect; @@ -26,8 +26,8 @@ protected void check(StaticAnalysis staticAnalysis) { public void process(CtCatch ctCatch) { if (ctCatch.isImplicit() || !ctCatch.getPosition().isValidPosition()) return; - List statements = SpoonUtil.getEffectiveStatements(ctCatch.getBody()); - Optional singleEffect = SpoonUtil.getSingleEffect(statements); + List statements = StatementUtil.getEffectiveStatements(ctCatch.getBody()); + Optional singleEffect = StatementUtil.getSingleEffect(statements); CtVariableReference caughtVariable = ctCatch.getParameter().getReference(); diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/check/complexity/RedundantElse.java b/autograder-core/src/main/java/de/firemage/autograder/core/check/complexity/RedundantElse.java index 7d41af17..a8b13a75 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/check/complexity/RedundantElse.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/check/complexity/RedundantElse.java @@ -4,7 +4,7 @@ import de.firemage.autograder.core.ProblemType; import de.firemage.autograder.core.check.ExecutableCheck; import de.firemage.autograder.core.integrated.IntegratedCheck; -import de.firemage.autograder.core.integrated.SpoonUtil; +import de.firemage.autograder.core.integrated.StatementUtil; import de.firemage.autograder.core.integrated.StaticAnalysis; import de.firemage.autograder.core.integrated.effects.Effect; import de.firemage.autograder.core.integrated.effects.TerminalEffect; @@ -21,11 +21,11 @@ public class RedundantElse extends IntegratedCheck { private static final int MINIMUM_STATEMENTS = 5; private Optional getTerminalEffect(CtStatement ctStatement) { - List ctStatements = SpoonUtil.getEffectiveStatements(ctStatement); + List ctStatements = StatementUtil.getEffectiveStatements(ctStatement); if (ctStatements.isEmpty()) return Optional.empty(); - Optional optionalEffect = SpoonUtil.tryMakeEffect(ctStatements.get(ctStatements.size() - 1)); + Optional optionalEffect = StatementUtil.tryMakeEffect(ctStatements.get(ctStatements.size() - 1)); return optionalEffect.flatMap(effect -> { if (effect instanceof TerminalEffect terminalEffect) { @@ -49,7 +49,7 @@ private void checkCtIf(CtIf ctIf) { } String elseIf = ""; - List elseStatements = SpoonUtil.getEffectiveStatements(ctIf.getElseStatement()); + List elseStatements = StatementUtil.getEffectiveStatements(ctIf.getElseStatement()); int numberOfElseStatements = elseStatements.size(); if (elseStatements.size() == 1 && elseStatements.get(0) instanceof CtIf ctElseIf) { // skip else { if ... } @@ -67,7 +67,7 @@ private void checkCtIf(CtIf ctIf) { return; } - numberOfElseStatements = SpoonUtil.getEffectiveStatements(ctElseIf.getThenStatement()).size(); + numberOfElseStatements = StatementUtil.getEffectiveStatements(ctElseIf.getThenStatement()).size(); elseIf = " else if (b) { ... }"; } else if (ctIf.getElseStatement().isImplicit()) { @@ -105,7 +105,7 @@ public void process(CtIf ctIf) { if (parentIf != null && parentIf.getElseStatement() != null) { // if so, then check if the else statement is this if // (to avoid conflicts with else { if } can be else if {}) - List ctStatements = SpoonUtil.getEffectiveStatements(parentIf.getElseStatement()); + List ctStatements = StatementUtil.getEffectiveStatements(parentIf.getElseStatement()); if (ctStatements.size() == 1 && ctStatements.get(0).equals(ctIf)) { return; } diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/check/complexity/RedundantIfForBooleanCheck.java b/autograder-core/src/main/java/de/firemage/autograder/core/check/complexity/RedundantIfForBooleanCheck.java index 479eda7a..9cb14253 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/check/complexity/RedundantIfForBooleanCheck.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/check/complexity/RedundantIfForBooleanCheck.java @@ -3,8 +3,10 @@ import de.firemage.autograder.core.LocalizedMessage; import de.firemage.autograder.core.ProblemType; import de.firemage.autograder.core.check.ExecutableCheck; +import de.firemage.autograder.core.integrated.ExpressionUtil; +import de.firemage.autograder.core.integrated.FactoryUtil; import de.firemage.autograder.core.integrated.IntegratedCheck; -import de.firemage.autograder.core.integrated.SpoonUtil; +import de.firemage.autograder.core.integrated.StatementUtil; import de.firemage.autograder.core.integrated.StaticAnalysis; import de.firemage.autograder.core.integrated.effects.AssignmentEffect; import de.firemage.autograder.core.integrated.effects.Effect; @@ -39,8 +41,8 @@ private String makeSuggestion(CtExpression ctExpression, Effect thenEffect, E } private void checkIfElse(CtExpression condition, CtStatement thenStmt, CtStatement elseStmt) { - Effect thenEffect = SpoonUtil.tryMakeEffect(thenStmt).orElse(null); - Effect elseEffect = SpoonUtil.tryMakeEffect(elseStmt).orElse(null); + Effect thenEffect = StatementUtil.tryMakeEffect(thenStmt).orElse(null); + Effect elseEffect = StatementUtil.tryMakeEffect(elseStmt).orElse(null); // skip if they are not both return statements or both assignments to the same variable if (thenEffect == null @@ -56,12 +58,12 @@ private void checkIfElse(CtExpression condition, CtStatement thenStmt, CtStat CtExpression elseValue = elseEffect.value().get(); // skip if they do not assign or return a boolean expression - if (!SpoonUtil.isBoolean(thenValue) || !SpoonUtil.isBoolean(elseValue)) { + if (!ExpressionUtil.isBoolean(thenValue) || !ExpressionUtil.isBoolean(elseValue)) { return; } - Boolean thenLiteral = SpoonUtil.tryGetBooleanLiteral(thenValue).orElse(null); - Boolean elseLiteral = SpoonUtil.tryGetBooleanLiteral(elseValue).orElse(null); + Boolean thenLiteral = ExpressionUtil.tryGetBooleanLiteral(thenValue).orElse(null); + Boolean elseLiteral = ExpressionUtil.tryGetBooleanLiteral(elseValue).orElse(null); // skip non-sense like if (a) return true else return true if (thenLiteral != null && thenLiteral.equals(elseLiteral)) { @@ -72,7 +74,7 @@ private void checkIfElse(CtExpression condition, CtStatement thenStmt, CtStat if (thenLiteral == null) { // if it does not return a literal, both the condition and the return value must be true - thenCondition = SpoonUtil.createBinaryOperator( + thenCondition = FactoryUtil.createBinaryOperator( condition, thenValue, BinaryOperatorKind.AND @@ -90,13 +92,13 @@ private void checkIfElse(CtExpression condition, CtStatement thenStmt, CtStat // if (a) { return b; } else { return c; } -> return a && b || c; if (thenCondition == null) { - combinedCondition = SpoonUtil.createBinaryOperator( - SpoonUtil.negate(condition), + combinedCondition = FactoryUtil.createBinaryOperator( + ExpressionUtil.negate(condition), elseValue, BinaryOperatorKind.AND ); } else { - combinedCondition = SpoonUtil.createBinaryOperator( + combinedCondition = FactoryUtil.createBinaryOperator( combinedCondition, elseValue, BinaryOperatorKind.OR @@ -105,11 +107,11 @@ private void checkIfElse(CtExpression condition, CtStatement thenStmt, CtStat } else if (elseLiteral) { // if it does return true, then the if condition must be false if (thenCondition == null) { - combinedCondition = SpoonUtil.negate(condition); + combinedCondition = ExpressionUtil.negate(condition); } else { - combinedCondition = SpoonUtil.createBinaryOperator( + combinedCondition = FactoryUtil.createBinaryOperator( combinedCondition, - SpoonUtil.negate(condition), + ExpressionUtil.negate(condition), BinaryOperatorKind.OR ); } @@ -137,7 +139,7 @@ public void process(CtBlock block) { return; } - List statements = SpoonUtil.getEffectiveStatements(block); + List statements = StatementUtil.getEffectiveStatements(block); // TODO: write test @@ -148,7 +150,7 @@ public void process(CtBlock block) { continue; } - CtStatement thenStatement = SpoonUtil.unwrapStatement(ifStmt.getThenStatement()); + CtStatement thenStatement = StatementUtil.unwrapStatement(ifStmt.getThenStatement()); CtStatement elseStatement = ifStmt.getElseStatement(); // if(...) { return true } return false @@ -163,7 +165,7 @@ public void process(CtBlock block) { checkIfElse( ifStmt.getCondition(), thenStatement, - SpoonUtil.unwrapStatement(elseStatement) + StatementUtil.unwrapStatement(elseStatement) ); } } diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/check/complexity/RedundantNegationCheck.java b/autograder-core/src/main/java/de/firemage/autograder/core/check/complexity/RedundantNegationCheck.java index 761f1306..a5a51d11 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/check/complexity/RedundantNegationCheck.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/check/complexity/RedundantNegationCheck.java @@ -3,8 +3,8 @@ import de.firemage.autograder.core.LocalizedMessage; import de.firemage.autograder.core.ProblemType; import de.firemage.autograder.core.check.ExecutableCheck; +import de.firemage.autograder.core.integrated.ExpressionUtil; import de.firemage.autograder.core.integrated.IntegratedCheck; -import de.firemage.autograder.core.integrated.SpoonUtil; import de.firemage.autograder.core.integrated.StaticAnalysis; import de.firemage.autograder.core.integrated.evaluator.Evaluator; @@ -30,7 +30,7 @@ public CtExpression foldCtUnaryOperator(CtUnaryOperator ctUnaryOperato CtExpression operand = ctUnaryOperator.getOperand(); // this negates the operand and optimizes it if possible - return (CtExpression) SpoonUtil.negate(operand); + return (CtExpression) ExpressionUtil.negate(operand); } } diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/check/complexity/RedundantUninitializedVariable.java b/autograder-core/src/main/java/de/firemage/autograder/core/check/complexity/RedundantUninitializedVariable.java index 02f2d813..ea46f1b1 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/check/complexity/RedundantUninitializedVariable.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/check/complexity/RedundantUninitializedVariable.java @@ -4,7 +4,7 @@ import de.firemage.autograder.core.ProblemType; import de.firemage.autograder.core.check.ExecutableCheck; import de.firemage.autograder.core.integrated.IntegratedCheck; -import de.firemage.autograder.core.integrated.SpoonUtil; +import de.firemage.autograder.core.integrated.StatementUtil; import de.firemage.autograder.core.integrated.StaticAnalysis; import spoon.processing.AbstractProcessor; import spoon.reflect.code.CtAssignment; @@ -51,7 +51,7 @@ public void process(CtLocalVariable ctLocalVariable) { if (ctAssignments.isEmpty()) return; - boolean isConditional = !SpoonUtil.getEffectiveStatements(ctLocalVariable.getParent(CtBlock.class)) + boolean isConditional = !StatementUtil.getEffectiveStatements(ctLocalVariable.getParent(CtBlock.class)) .contains(ctAssignments.get(0)); if (isConditional) { diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/check/complexity/RedundantVariable.java b/autograder-core/src/main/java/de/firemage/autograder/core/check/complexity/RedundantVariable.java index 6e98f2ea..a3654380 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/check/complexity/RedundantVariable.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/check/complexity/RedundantVariable.java @@ -5,7 +5,7 @@ import de.firemage.autograder.core.ProblemType; import de.firemage.autograder.core.check.ExecutableCheck; import de.firemage.autograder.core.integrated.IntegratedCheck; -import de.firemage.autograder.core.integrated.SpoonUtil; +import de.firemage.autograder.core.integrated.StatementUtil; import de.firemage.autograder.core.integrated.StaticAnalysis; import de.firemage.autograder.core.integrated.UsesFinder; import spoon.reflect.code.CtComment; @@ -53,10 +53,10 @@ private void checkVariableRead(CtStatement ctStatement, CtVariableRead ctVari return; } - CtStatement previousStatement = SpoonUtil.getPreviousStatement(ctStatement).orElse(null); + CtStatement previousStatement = StatementUtil.getPreviousStatement(ctStatement).orElse(null); while (!ctLocalVariable.equals(previousStatement) && this.isAllowedStatement(previousStatement)) { - previousStatement = SpoonUtil.getPreviousStatement(previousStatement).orElse(null); + previousStatement = StatementUtil.getPreviousStatement(previousStatement).orElse(null); } if (previousStatement == null) { diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/check/complexity/RepeatedMathOperationCheck.java b/autograder-core/src/main/java/de/firemage/autograder/core/check/complexity/RepeatedMathOperationCheck.java index 15425d75..7febddc3 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/check/complexity/RepeatedMathOperationCheck.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/check/complexity/RepeatedMathOperationCheck.java @@ -3,9 +3,10 @@ import de.firemage.autograder.core.LocalizedMessage; import de.firemage.autograder.core.ProblemType; import de.firemage.autograder.core.check.ExecutableCheck; +import de.firemage.autograder.core.integrated.FactoryUtil; import de.firemage.autograder.core.integrated.IntegratedCheck; -import de.firemage.autograder.core.integrated.SpoonUtil; import de.firemage.autograder.core.integrated.StaticAnalysis; +import de.firemage.autograder.core.integrated.TypeUtil; import de.firemage.autograder.core.integrated.evaluator.Evaluator; import de.firemage.autograder.core.integrated.evaluator.fold.Fold; import spoon.processing.AbstractProcessor; @@ -90,7 +91,7 @@ public CtElement exit(CtElement ctElement) { public CtExpression foldCtBinaryOperator(CtBinaryOperator ctBinaryOperator) { // skip if the operator is not supported if (!OCCURRENCE_THRESHOLDS.containsKey(ctBinaryOperator.getKind()) || - !SpoonUtil.isPrimitiveNumeric(ctBinaryOperator.getType())) { + !TypeUtil.isPrimitiveNumeric(ctBinaryOperator.getType())) { return ctBinaryOperator; } @@ -112,7 +113,7 @@ public CtExpression foldCtBinaryOperator(CtBinaryOperator ctBinaryOper return this.function.apply(expression, count); } }) - .reduce((left, right) -> SpoonUtil.createBinaryOperator(left, right, this.kind)) + .reduce((left, right) -> FactoryUtil.createBinaryOperator(left, right, this.kind)) .orElseThrow(); } } @@ -125,7 +126,7 @@ public static CtExpression repeatExpression(BinaryOperatorKind kind, CtExpres } public static CtExpression joinExpressions(BinaryOperatorKind kind, CtExpression first, CtExpression... others) { - return Arrays.stream(others).reduce(first, (left, right) -> SpoonUtil.createBinaryOperator(left, right, kind)); + return Arrays.stream(others).reduce(first, (left, right) -> FactoryUtil.createBinaryOperator(left, right, kind)); } @Override @@ -148,9 +149,9 @@ public void process(CtExpression ctExpression) { OCCURRENCE_THRESHOLDS.get(BinaryOperatorKind.PLUS), (expression, count) -> { plusOptimizations.addAndGet(1); - return SpoonUtil.createBinaryOperator( + return FactoryUtil.createBinaryOperator( expression, - SpoonUtil.makeLiteralNumber(expression.getType(), count), + FactoryUtil.makeLiteralNumber(expression.getType(), count), BinaryOperatorKind.MUL ); } @@ -162,11 +163,11 @@ public void process(CtExpression ctExpression) { (expression, count) -> { TypeFactory typeFactory = expression.getFactory().Type(); mulOptimizations.addAndGet(1); - return SpoonUtil.createStaticInvocation( + return FactoryUtil.createStaticInvocation( typeFactory.get(java.lang.Math.class).getReference(), "pow", expression, - SpoonUtil.makeLiteralNumber(typeFactory.integerPrimitiveType(), count) + FactoryUtil.makeLiteralNumber(typeFactory.integerPrimitiveType(), count) ); } ); @@ -197,8 +198,8 @@ private Map countOccurrences(CtExpression expression, Bina if (expression instanceof CtBinaryOperator operator && operator.getKind() == kind) { // '+' can also be used on Strings, but String operations are not associative - if (SpoonUtil.isString(operator.getLeftHandOperand().getType()) || - SpoonUtil.isString(operator.getRightHandOperand().getType())) { + if (TypeUtil.isString(operator.getLeftHandOperand().getType()) || + TypeUtil.isString(operator.getRightHandOperand().getType())) { return Map.of(); } diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/check/complexity/TooManyExceptions.java b/autograder-core/src/main/java/de/firemage/autograder/core/check/complexity/TooManyExceptions.java index 5817ceb5..a3682f94 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/check/complexity/TooManyExceptions.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/check/complexity/TooManyExceptions.java @@ -4,8 +4,8 @@ import de.firemage.autograder.core.ProblemType; import de.firemage.autograder.core.check.ExecutableCheck; import de.firemage.autograder.core.integrated.IntegratedCheck; -import de.firemage.autograder.core.integrated.SpoonUtil; import de.firemage.autograder.core.integrated.StaticAnalysis; +import de.firemage.autograder.core.integrated.TypeUtil; import spoon.processing.AbstractProcessor; import spoon.reflect.declaration.CtClass; @@ -28,7 +28,7 @@ public void process(CtClass ctClass) { return; } - if (SpoonUtil.isSubtypeOf(ctClass.getReference(), java.lang.Exception.class)) { + if (TypeUtil.isSubtypeOf(ctClass.getReference(), java.lang.Exception.class)) { declaredExceptions.add(ctClass); } } diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/check/complexity/TryCatchComplexity.java b/autograder-core/src/main/java/de/firemage/autograder/core/check/complexity/TryCatchComplexity.java index 105e3647..9a8c9e99 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/check/complexity/TryCatchComplexity.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/check/complexity/TryCatchComplexity.java @@ -4,8 +4,8 @@ import de.firemage.autograder.core.ProblemType; import de.firemage.autograder.core.check.ExecutableCheck; import de.firemage.autograder.core.integrated.IntegratedCheck; -import de.firemage.autograder.core.integrated.SpoonUtil; import de.firemage.autograder.core.integrated.StaticAnalysis; +import de.firemage.autograder.core.integrated.MethodUtil; import spoon.reflect.code.CtBlock; import spoon.reflect.code.CtCase; import spoon.reflect.code.CtCatch; @@ -66,12 +66,12 @@ private void visitNestedStatement(CtTry ctTry, List allStatements) } private List filterMethodInvocations(List statements) { return statements.stream() - .filter(statement -> !SpoonUtil.isInvocation(statement)) + .filter(statement -> !MethodUtil.isInvocation(statement)) .toList(); } private List extractMethodInvocations(List statements) { - return statements.stream().filter(SpoonUtil::isInvocation).toList(); + return statements.stream().filter(MethodUtil::isInvocation).toList(); } private void visitStatement(CtStatement statement, List allStatements) { // The CtStatement may be null if the body of an if statement is empty. for example "if (true);" @@ -88,7 +88,7 @@ public void visitCtTry(CtTry ctTry) { visitNestedStatement(ctTry, allStatements); List catchers = ctTry.getCatchers(); catchers.stream().flatMap(catcher -> catcher.getBody().getStatements().stream()) - .filter(statement -> !SpoonUtil.isInvocation(statement)) + .filter(statement -> !MethodUtil.isInvocation(statement)) .forEach(statement -> visitStatement(statement, allStatements)); allStatements.addAll(extractMethodInvocations(catchers diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/check/complexity/UnnecessaryBoxing.java b/autograder-core/src/main/java/de/firemage/autograder/core/check/complexity/UnnecessaryBoxing.java index 485eaec6..1febaaac 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/check/complexity/UnnecessaryBoxing.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/check/complexity/UnnecessaryBoxing.java @@ -4,8 +4,8 @@ import de.firemage.autograder.core.LocalizedMessage; import de.firemage.autograder.core.ProblemType; import de.firemage.autograder.core.check.ExecutableCheck; +import de.firemage.autograder.core.integrated.ExpressionUtil; import de.firemage.autograder.core.integrated.IntegratedCheck; -import de.firemage.autograder.core.integrated.SpoonUtil; import de.firemage.autograder.core.integrated.StaticAnalysis; import de.firemage.autograder.core.integrated.UsesFinder; import spoon.processing.AbstractProcessor; @@ -26,7 +26,7 @@ private static boolean isBoxedType(CtTypeReference ctTypeReference) { private static boolean isLikelyNull(CtExpression ctExpression) { return ctExpression == null - || SpoonUtil.isNullLiteral(ctExpression) + || ExpressionUtil.isNullLiteral(ctExpression) || isBoxedType(ctExpression.getType()); } diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/check/complexity/UnusedImport.java b/autograder-core/src/main/java/de/firemage/autograder/core/check/complexity/UnusedImport.java index 76143880..3fad6441 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/check/complexity/UnusedImport.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/check/complexity/UnusedImport.java @@ -3,9 +3,12 @@ import de.firemage.autograder.core.LocalizedMessage; import de.firemage.autograder.core.ProblemType; import de.firemage.autograder.core.check.ExecutableCheck; +import de.firemage.autograder.core.integrated.CoreUtil; import de.firemage.autograder.core.integrated.IntegratedCheck; -import de.firemage.autograder.core.integrated.SpoonUtil; +import de.firemage.autograder.core.integrated.VariableUtil; import de.firemage.autograder.core.integrated.StaticAnalysis; +import de.firemage.autograder.core.integrated.ElementUtil; +import de.firemage.autograder.core.integrated.TypeUtil; import de.firemage.autograder.core.integrated.UsesFinder; import spoon.javadoc.api.elements.JavadocReference; import spoon.javadoc.api.elements.JavadocVisitor; @@ -62,7 +65,7 @@ private static boolean isJavaLangImport(CtImport ctImport) { } private static boolean isInnerType(CtElement ctElement) { - return ctElement instanceof CtType ctType && SpoonUtil.isInnerClass(ctType); + return ctElement instanceof CtType ctType && TypeUtil.isInnerClass(ctType); } private void reportProblem(CtImport ctImport) { @@ -93,7 +96,7 @@ public List visitReference(JavadocReference reference) { } private static boolean isReferencingTheSameElement(CtReference left, CtReference right) { - return left.equals(right) || Objects.equals(SpoonUtil.getReferenceDeclaration(left), SpoonUtil.getReferenceDeclaration(right)); + return left.equals(right) || Objects.equals(VariableUtil.getReferenceDeclaration(left), VariableUtil.getReferenceDeclaration(right)); } @SuppressWarnings("unchecked") @@ -116,7 +119,7 @@ private static boolean hasAnyJavadocUses(CtReference ctReference, Filter ctTypeReference && SpoonUtil.isInnerClass(ctTypeReference.getTypeDeclaration()))) { + if (isJavaLangImport(ctImport) && !(ctImport.getReference() instanceof CtTypeReference ctTypeReference && TypeUtil.isInnerClass(ctTypeReference.getTypeDeclaration()))) { this.reportProblem(ctImport); return; } - CtNamedElement element = (CtNamedElement) SpoonUtil.getReferenceDeclaration(ctImport.getReference()); + CtNamedElement element = (CtNamedElement) VariableUtil.getReferenceDeclaration(ctImport.getReference()); // types from the same package are imported implicitly // @@ -169,7 +172,7 @@ private void checkImport(CtImport ctImport, CtCompilationUnit ctCompilationUnit, } Predicate isSameFile = ctElement -> { - SourcePosition position = SpoonUtil.findPosition(ctElement); + SourcePosition position = ElementUtil.findPosition(ctElement); return position != null && position.getCompilationUnit().equals(ctCompilationUnit); }; @@ -211,7 +214,7 @@ private void checkImport(CtImport ctImport, CtCompilationUnit ctCompilationUnit, @Override protected void check(StaticAnalysis staticAnalysis) { - SpoonUtil.visitCtCompilationUnit(staticAnalysis.getModel(), ctCompilationUnit -> { + CoreUtil.visitCtCompilationUnit(staticAnalysis.getModel(), ctCompilationUnit -> { Collection importedElements = new HashSet<>(); for (CtImport ctImport : ctCompilationUnit.getImports()) { diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/check/complexity/WrapperInstantiationCheck.java b/autograder-core/src/main/java/de/firemage/autograder/core/check/complexity/WrapperInstantiationCheck.java index 1b8b8679..6284246b 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/check/complexity/WrapperInstantiationCheck.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/check/complexity/WrapperInstantiationCheck.java @@ -3,9 +3,10 @@ import de.firemage.autograder.core.LocalizedMessage; import de.firemage.autograder.core.ProblemType; import de.firemage.autograder.core.check.ExecutableCheck; +import de.firemage.autograder.core.integrated.ExpressionUtil; import de.firemage.autograder.core.integrated.IntegratedCheck; -import de.firemage.autograder.core.integrated.SpoonUtil; import de.firemage.autograder.core.integrated.StaticAnalysis; +import de.firemage.autograder.core.integrated.TypeUtil; import spoon.processing.AbstractProcessor; import spoon.reflect.code.CtConstructorCall; import spoon.reflect.code.CtExpression; @@ -16,7 +17,7 @@ @ExecutableCheck(reportedProblems = {ProblemType.PRIMITIVE_WRAPPER_INSTANTIATION}) public class WrapperInstantiationCheck extends IntegratedCheck { private static boolean isPrimitiveWrapper(CtTypeReference ctTypeReference) { - return SpoonUtil.isTypeEqualTo( + return TypeUtil.isTypeEqualTo( ctTypeReference, Double.class, Float.class, Long.class, Integer.class, @@ -43,7 +44,7 @@ public void process(CtConstructorCall ctConstructorCall) { // check if the argument is not the unboxed type // for example Integer(String) - if (!boxedType.unbox().equals(SpoonUtil.getExpressionType(value))) { + if (!boxedType.unbox().equals(ExpressionUtil.getExpressionType(value))) { suggestion = "%s.valueOf(%s)".formatted(boxedType, suggestion); } diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/check/debug/PrintStackTraceCheck.java b/autograder-core/src/main/java/de/firemage/autograder/core/check/debug/PrintStackTraceCheck.java index d9424a00..6a18ef0a 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/check/debug/PrintStackTraceCheck.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/check/debug/PrintStackTraceCheck.java @@ -4,8 +4,8 @@ import de.firemage.autograder.core.ProblemType; import de.firemage.autograder.core.check.ExecutableCheck; import de.firemage.autograder.core.integrated.IntegratedCheck; -import de.firemage.autograder.core.integrated.SpoonUtil; import de.firemage.autograder.core.integrated.StaticAnalysis; +import de.firemage.autograder.core.integrated.TypeUtil; import spoon.processing.AbstractProcessor; import spoon.reflect.code.CtInvocation; import spoon.reflect.code.CtVariableRead; @@ -17,7 +17,7 @@ private static boolean hasInvokedPrintStackTrace(CtInvocation ctInvocation) { // workaround for https://github.com/INRIA/spoon/issues/5414 && ctVariableRead.getType().getTypeDeclaration() != null // ensure the method is called on the correct type - && SpoonUtil.isSubtypeOf(ctVariableRead.getType(), java.lang.Throwable.class) + && TypeUtil.isSubtypeOf(ctVariableRead.getType(), java.lang.Throwable.class) && ctInvocation.getExecutable().getSimpleName().equals("printStackTrace"); } diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/check/exceptions/ExceptionControlFlowCheck.java b/autograder-core/src/main/java/de/firemage/autograder/core/check/exceptions/ExceptionControlFlowCheck.java index 3153478e..1b8a6dbb 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/check/exceptions/ExceptionControlFlowCheck.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/check/exceptions/ExceptionControlFlowCheck.java @@ -4,8 +4,8 @@ import de.firemage.autograder.core.ProblemType; import de.firemage.autograder.core.check.ExecutableCheck; import de.firemage.autograder.core.integrated.IntegratedCheck; -import de.firemage.autograder.core.integrated.SpoonUtil; import de.firemage.autograder.core.integrated.StaticAnalysis; +import de.firemage.autograder.core.integrated.TypeUtil; import spoon.reflect.code.CtCatch; import spoon.reflect.code.CtThrow; import spoon.reflect.code.CtTry; @@ -59,7 +59,7 @@ public void visitCtCatch(CtCatch ctCatch) { } for (CtTypeReference ctTypeReference : ctCatch.getParameter().getMultiTypes()) { - if (SpoonUtil.isTypeEqualTo( + if (TypeUtil.isTypeEqualTo( ctTypeReference, java.lang.NullPointerException.class, java.lang.ArithmeticException.class, diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/check/exceptions/ExceptionMessageCheck.java b/autograder-core/src/main/java/de/firemage/autograder/core/check/exceptions/ExceptionMessageCheck.java index 65ff8935..05388fdf 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/check/exceptions/ExceptionMessageCheck.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/check/exceptions/ExceptionMessageCheck.java @@ -4,9 +4,10 @@ import de.firemage.autograder.core.ProblemType; import de.firemage.autograder.core.check.ExecutableCheck; +import de.firemage.autograder.core.integrated.ExpressionUtil; import de.firemage.autograder.core.integrated.IntegratedCheck; -import de.firemage.autograder.core.integrated.SpoonUtil; import de.firemage.autograder.core.integrated.StaticAnalysis; +import de.firemage.autograder.core.integrated.TypeUtil; import spoon.processing.AbstractProcessor; import spoon.reflect.code.CtCase; import spoon.reflect.code.CtConstructorCall; @@ -20,7 +21,7 @@ public class ExceptionMessageCheck extends IntegratedCheck { private static boolean isExceptionWithoutMessage(CtExpression expression) { if (!(expression instanceof CtConstructorCall ctConstructorCall) - || !SpoonUtil.isSubtypeOf(expression.getType(), java.lang.Exception.class)) { + || !TypeUtil.isSubtypeOf(expression.getType(), java.lang.Exception.class)) { return false; } @@ -44,11 +45,11 @@ && hasMessage(ctInvocation.getArguments()) private static boolean hasMessage(Iterable> arguments) { for (CtExpression ctExpression : arguments) { // consider a passed throwable as having message - if (SpoonUtil.isSubtypeOf(ctExpression.getType(), java.lang.Throwable.class)) { + if (TypeUtil.isSubtypeOf(ctExpression.getType(), java.lang.Throwable.class)) { return true; } - String literal = SpoonUtil.tryGetStringLiteral(ctExpression).orElse(null); + String literal = ExpressionUtil.tryGetStringLiteral(ctExpression).orElse(null); return literal == null || !literal.isBlank(); } diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/check/exceptions/NumberFormatExceptionIgnored.java b/autograder-core/src/main/java/de/firemage/autograder/core/check/exceptions/NumberFormatExceptionIgnored.java index 229d1b8b..93a14ae5 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/check/exceptions/NumberFormatExceptionIgnored.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/check/exceptions/NumberFormatExceptionIgnored.java @@ -4,8 +4,9 @@ import de.firemage.autograder.core.ProblemType; import de.firemage.autograder.core.check.ExecutableCheck; import de.firemage.autograder.core.integrated.IntegratedCheck; -import de.firemage.autograder.core.integrated.SpoonUtil; import de.firemage.autograder.core.integrated.StaticAnalysis; +import de.firemage.autograder.core.integrated.MethodUtil; +import de.firemage.autograder.core.integrated.TypeUtil; import spoon.processing.AbstractProcessor; import spoon.reflect.code.CtCatch; import spoon.reflect.code.CtInvocation; @@ -21,7 +22,7 @@ private static boolean isNFECaught(CtInvocation ctInvocation) { return ctInvocation.getParent(new CompositeFilter<>( FilteringOperator.INTERSECTION, new TypeFilter<>(CtTry.class), - ctTry -> ctTry.getCatchers().stream().anyMatch((CtCatch ctCatch) -> SpoonUtil.isTypeEqualTo(ctCatch.getParameter().getType(), NumberFormatException.class)) + ctTry -> ctTry.getCatchers().stream().anyMatch((CtCatch ctCatch) -> TypeUtil.isTypeEqualTo(ctCatch.getParameter().getType(), NumberFormatException.class)) )) != null; } @Override @@ -33,7 +34,7 @@ public void process(CtInvocation ctInvocation) { return; } - if (SpoonUtil.isSignatureEqualTo(ctInvocation.getExecutable(), int.class, "parseInt", String.class) && !isNFECaught(ctInvocation)) { + if (MethodUtil.isSignatureEqualTo(ctInvocation.getExecutable(), int.class, "parseInt", String.class) && !isNFECaught(ctInvocation)) { addLocalProblem( ctInvocation, new LocalizedMessage("number-format-exception-ignored"), diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/check/exceptions/RuntimeExceptionCatchCheck.java b/autograder-core/src/main/java/de/firemage/autograder/core/check/exceptions/RuntimeExceptionCatchCheck.java index d869bfa2..ec20e0c4 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/check/exceptions/RuntimeExceptionCatchCheck.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/check/exceptions/RuntimeExceptionCatchCheck.java @@ -4,7 +4,7 @@ import de.firemage.autograder.core.ProblemType; import de.firemage.autograder.core.check.ExecutableCheck; import de.firemage.autograder.core.integrated.IntegratedCheck; -import de.firemage.autograder.core.integrated.SpoonUtil; +import de.firemage.autograder.core.integrated.StatementUtil; import de.firemage.autograder.core.integrated.StaticAnalysis; import spoon.processing.AbstractProcessor; import spoon.reflect.code.CtCatch; @@ -28,7 +28,7 @@ public void process(CtCatch ctCatch) { CtTypeReference varType = ctCatch.getParameter().getType(); - List statements = SpoonUtil.getEffectiveStatements(ctCatch.getBody()); + List statements = StatementUtil.getEffectiveStatements(ctCatch.getBody()); // catching an exception to throw another is okay if (statements.size() == 1 && statements.get(0) instanceof CtThrow) { return; diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/check/general/AvoidRecompilingRegex.java b/autograder-core/src/main/java/de/firemage/autograder/core/check/general/AvoidRecompilingRegex.java index a48855ef..de215539 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/check/general/AvoidRecompilingRegex.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/check/general/AvoidRecompilingRegex.java @@ -3,9 +3,10 @@ import de.firemage.autograder.core.LocalizedMessage; import de.firemage.autograder.core.ProblemType; import de.firemage.autograder.core.check.ExecutableCheck; +import de.firemage.autograder.core.integrated.ExpressionUtil; import de.firemage.autograder.core.integrated.IntegratedCheck; -import de.firemage.autograder.core.integrated.SpoonUtil; import de.firemage.autograder.core.integrated.StaticAnalysis; +import de.firemage.autograder.core.integrated.TypeUtil; import de.firemage.autograder.core.integrated.UsesFinder; import spoon.processing.AbstractProcessor; import spoon.reflect.code.CtExpression; @@ -21,7 +22,7 @@ public class AvoidRecompilingRegex extends IntegratedCheck { private boolean isPatternInvocation(CtInvocation ctInvocation) { return ctInvocation.getTarget() instanceof CtTypeAccess ctTypeAccess - && SpoonUtil.isTypeEqualTo(ctTypeAccess.getAccessedType(), java.util.regex.Pattern.class) + && TypeUtil.isTypeEqualTo(ctTypeAccess.getAccessedType(), java.util.regex.Pattern.class) && List.of("matches", "compile").contains(ctInvocation.getExecutable().getSimpleName()); } @@ -32,12 +33,12 @@ protected void check(StaticAnalysis staticAnalysis) { public void process(CtField ctField) { if (ctField.isImplicit() || !ctField.getPosition().isValidPosition() - || !SpoonUtil.isTypeEqualTo(ctField.getType(), String.class) + || !TypeUtil.isTypeEqualTo(ctField.getType(), String.class) || ctField.getDefaultExpression() == null) { return; } - CtExpression ctExpression = SpoonUtil.resolveCtExpression(ctField.getDefaultExpression()); + CtExpression ctExpression = ExpressionUtil.resolveCtExpression(ctField.getDefaultExpression()); // skip all non-literals to improve performance if (!(ctExpression instanceof CtLiteral)) { diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/check/general/AvoidShadowing.java b/autograder-core/src/main/java/de/firemage/autograder/core/check/general/AvoidShadowing.java index 463a65ff..0fe7e598 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/check/general/AvoidShadowing.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/check/general/AvoidShadowing.java @@ -4,8 +4,8 @@ import de.firemage.autograder.core.ProblemType; import de.firemage.autograder.core.check.ExecutableCheck; import de.firemage.autograder.core.integrated.IntegratedCheck; -import de.firemage.autograder.core.integrated.SpoonUtil; import de.firemage.autograder.core.integrated.StaticAnalysis; +import de.firemage.autograder.core.integrated.MethodUtil; import de.firemage.autograder.core.integrated.UsesFinder; import spoon.processing.AbstractProcessor; import spoon.reflect.declaration.CtConstructor; @@ -58,7 +58,7 @@ public void process(CtVariable ctVariable) { } // skip fields inside overridden methods - if (SpoonUtil.isInOverridingMethod(ctVariable) || SpoonUtil.isInSetter(ctVariable)) { + if (MethodUtil.isInOverridingMethod(ctVariable) || MethodUtil.isInSetter(ctVariable)) { return; } diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/check/general/BinaryOperator.java b/autograder-core/src/main/java/de/firemage/autograder/core/check/general/BinaryOperator.java index 8b50eb8c..30e858ea 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/check/general/BinaryOperator.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/check/general/BinaryOperator.java @@ -4,8 +4,8 @@ import de.firemage.autograder.core.ProblemType; import de.firemage.autograder.core.check.ExecutableCheck; import de.firemage.autograder.core.integrated.IntegratedCheck; -import de.firemage.autograder.core.integrated.SpoonUtil; import de.firemage.autograder.core.integrated.StaticAnalysis; +import de.firemage.autograder.core.integrated.TypeUtil; import spoon.processing.AbstractProcessor; import spoon.reflect.code.BinaryOperatorKind; import spoon.reflect.code.CtBinaryOperator; @@ -32,11 +32,11 @@ public void process(CtBinaryOperator ctBinaryOperator) { } boolean hasBinaryOperator = BINARY_OPERATORS.contains(ctBinaryOperator.getKind()) - && SpoonUtil.isTypeEqualTo(ctBinaryOperator.getType(), boolean.class, Boolean.class); + && TypeUtil.isTypeEqualTo(ctBinaryOperator.getType(), boolean.class, Boolean.class); if (!hasBinaryOperator) { hasBinaryOperator = ctBinaryOperator.getElements(new TypeFilter<>(CtBinaryOperator.class)).stream() - .anyMatch(operator -> SpoonUtil.isTypeEqualTo(operator.getType(), boolean.class, Boolean.class) + .anyMatch(operator -> TypeUtil.isTypeEqualTo(operator.getType(), boolean.class, Boolean.class) && BINARY_OPERATORS.contains(operator.getKind())); } diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/check/general/CompareCharValue.java b/autograder-core/src/main/java/de/firemage/autograder/core/check/general/CompareCharValue.java index f553bc42..698a74c3 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/check/general/CompareCharValue.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/check/general/CompareCharValue.java @@ -3,9 +3,10 @@ import de.firemage.autograder.core.LocalizedMessage; import de.firemage.autograder.core.ProblemType; import de.firemage.autograder.core.check.ExecutableCheck; +import de.firemage.autograder.core.integrated.ExpressionUtil; import de.firemage.autograder.core.integrated.IntegratedCheck; -import de.firemage.autograder.core.integrated.SpoonUtil; import de.firemage.autograder.core.integrated.StaticAnalysis; +import de.firemage.autograder.core.integrated.TypeUtil; import spoon.processing.AbstractProcessor; import spoon.reflect.code.BinaryOperatorKind; import spoon.reflect.code.CtBinaryOperator; @@ -32,8 +33,8 @@ public class CompareCharValue extends IntegratedCheck { ); private static Optional getComparedIntegerValue(CtExpression left, CtExpression right) { - if (!SpoonUtil.isTypeEqualTo(left.getType(), char.class) - || !(SpoonUtil.resolveConstant(right) instanceof CtLiteral literal && literal.getValue() instanceof Integer value)) { + if (!TypeUtil.isTypeEqualTo(left.getType(), char.class) + || !(ExpressionUtil.resolveConstant(right) instanceof CtLiteral literal && literal.getValue() instanceof Integer value)) { return Optional.empty(); } diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/check/general/CompareObjectsNotStringsCheck.java b/autograder-core/src/main/java/de/firemage/autograder/core/check/general/CompareObjectsNotStringsCheck.java index b6611bf1..3d6949fe 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/check/general/CompareObjectsNotStringsCheck.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/check/general/CompareObjectsNotStringsCheck.java @@ -3,8 +3,8 @@ import de.firemage.autograder.core.LocalizedMessage; import de.firemage.autograder.core.ProblemType; import de.firemage.autograder.core.check.ExecutableCheck; +import de.firemage.autograder.core.integrated.ExpressionUtil; import de.firemage.autograder.core.integrated.IntegratedCheck; -import de.firemage.autograder.core.integrated.SpoonUtil; import de.firemage.autograder.core.integrated.StaticAnalysis; import spoon.processing.AbstractProcessor; import spoon.reflect.code.CtInvocation; @@ -28,12 +28,12 @@ public void process(CtInvocation invocation) { CtExecutableReference executable = invocation.getExecutable(); if (executable.getSignature().equals("equals(java.lang.Object)") && executable.getParameters().size() == 1) { - Optional> lhsType = SpoonUtil.isToStringCall(invocation.getTarget()); + Optional> lhsType = ExpressionUtil.isToStringCall(invocation.getTarget()); if (lhsType.isEmpty()) { return; } - Optional> rhsType = SpoonUtil.isToStringCall(invocation.getArguments().get(0)); + Optional> rhsType = ExpressionUtil.isToStringCall(invocation.getArguments().get(0)); if (rhsType.isEmpty()) { return; } diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/check/general/FieldShouldBeFinal.java b/autograder-core/src/main/java/de/firemage/autograder/core/check/general/FieldShouldBeFinal.java index 34c36b0b..f194c73f 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/check/general/FieldShouldBeFinal.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/check/general/FieldShouldBeFinal.java @@ -4,8 +4,9 @@ import de.firemage.autograder.core.ProblemType; import de.firemage.autograder.core.check.ExecutableCheck; import de.firemage.autograder.core.integrated.IntegratedCheck; -import de.firemage.autograder.core.integrated.SpoonUtil; +import de.firemage.autograder.core.integrated.StatementUtil; import de.firemage.autograder.core.integrated.StaticAnalysis; +import de.firemage.autograder.core.integrated.TypeUtil; import de.firemage.autograder.core.integrated.UsesFinder; import spoon.processing.AbstractProcessor; import spoon.reflect.code.CtAssignment; @@ -39,7 +40,7 @@ private static boolean canBeFinal(CtField ctField) { return false; } - if (ctField.isProtected() && SpoonUtil.hasSubtype(ctClass)) { + if (ctField.isProtected() && TypeUtil.hasSubtype(ctClass)) { return false; } @@ -93,7 +94,7 @@ private static boolean canBeFinal(CtField ctField) { int mainPathWrites = 0; int otherPathWrites = 0; - for (CtStatement ctStatement : SpoonUtil.getEffectiveStatementsOf(ctConstructor)) { + for (CtStatement ctStatement : StatementUtil.getEffectiveStatementsOf(ctConstructor)) { if (ctStatement instanceof CtAssignment ctAssignment && UsesFinder.variableWrites(ctField).nestedIn(ctAssignment).hasAny()) { mainPathWrites += 1; } else if (UsesFinder.variableWrites(ctField).nestedIn(ctStatement).hasAny()) { diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/check/general/ForToForEachLoop.java b/autograder-core/src/main/java/de/firemage/autograder/core/check/general/ForToForEachLoop.java index e557bb5a..519013b2 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/check/general/ForToForEachLoop.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/check/general/ForToForEachLoop.java @@ -6,8 +6,9 @@ import de.firemage.autograder.core.integrated.ForLoopRange; import de.firemage.autograder.core.integrated.IntegratedCheck; -import de.firemage.autograder.core.integrated.SpoonUtil; import de.firemage.autograder.core.integrated.StaticAnalysis; +import de.firemage.autograder.core.integrated.MethodUtil; +import de.firemage.autograder.core.integrated.TypeUtil; import de.firemage.autograder.core.integrated.UsesFinder; import spoon.processing.AbstractProcessor; import spoon.reflect.code.CtArrayRead; @@ -32,7 +33,7 @@ public class ForToForEachLoop extends IntegratedCheck { private static final Function, Optional>> LOOP_VARIABLE_ACCESS_STRING = ctVariableAccess -> { if (ctVariableAccess.getParent() instanceof CtInvocation ctInvocation - && SpoonUtil.isSignatureEqualTo(ctInvocation.getExecutable(), char.class, "charAt", int.class) + && MethodUtil.isSignatureEqualTo(ctInvocation.getExecutable(), char.class, "charAt", int.class) && ctInvocation.getTarget() instanceof CtVariableAccess variableAccess) { return Optional.of(variableAccess); } @@ -54,7 +55,7 @@ public class ForToForEachLoop extends IntegratedCheck { // && SpoonUtil.isSignatureEqualTo(ctInvocation.getExecutable(), Object.class, "get", int.class) && ctInvocation.getExecutable().getSimpleName().equals("get") && ctInvocation.getTarget() instanceof CtVariableAccess variableAccess - && SpoonUtil.isSubtypeOf(variableAccess.getType(), java.util.List.class)) { + && TypeUtil.isSubtypeOf(variableAccess.getType(), java.util.List.class)) { return Optional.of(variableAccess); } @@ -127,17 +128,17 @@ public static Optional> findIterable(ForLoopRange forLoopRange) { // check if the end condition is collection.size() if (end instanceof CtInvocation ctInvocation - && SpoonUtil.isSignatureEqualTo(ctInvocation.getExecutable(), int.class, "size") + && MethodUtil.isSignatureEqualTo(ctInvocation.getExecutable(), int.class, "size") && ctInvocation.getTarget() instanceof CtVariableAccess target - && SpoonUtil.isSubtypeOf(target.getType(), java.util.Collection.class)) { + && TypeUtil.isSubtypeOf(target.getType(), java.util.Collection.class)) { return Optional.ofNullable(target.getVariable().getDeclaration()); } // check if the end condition is string.length() if (end instanceof CtInvocation ctInvocation - && SpoonUtil.isSignatureEqualTo(ctInvocation.getExecutable(), int.class, "length") + && MethodUtil.isSignatureEqualTo(ctInvocation.getExecutable(), int.class, "length") && ctInvocation.getTarget() instanceof CtVariableAccess target - && SpoonUtil.isTypeEqualTo(target.getType(), java.lang.String.class)) { + && TypeUtil.isTypeEqualTo(target.getType(), java.lang.String.class)) { return Optional.ofNullable(target.getVariable().getDeclaration()); } @@ -173,12 +174,12 @@ public void process(CtFor ctFor) { String iterableExpression = iterable.getSimpleName(); Function, Optional>> getPotentialLoopVariableAccess; - if (SpoonUtil.isString(iterable.getType())) { + if (TypeUtil.isString(iterable.getType())) { getPotentialLoopVariableAccess = LOOP_VARIABLE_ACCESS_STRING; iterableExpression = "%s.toCharArray()".formatted(iterableExpression); elementType = ctFor.getFactory().createCtTypeReference(char.class); - } else if (SpoonUtil.isSubtypeOf(iterable.getType(), java.util.List.class)) { + } else if (TypeUtil.isSubtypeOf(iterable.getType(), java.util.List.class)) { getPotentialLoopVariableAccess = LOOP_VARIABLE_ACCESS_LIST; // size != 1, if the list is a raw type: List list = new ArrayList(); diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/check/general/LoopShouldBeDoWhile.java b/autograder-core/src/main/java/de/firemage/autograder/core/check/general/LoopShouldBeDoWhile.java index 0288a866..00b11b25 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/check/general/LoopShouldBeDoWhile.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/check/general/LoopShouldBeDoWhile.java @@ -3,8 +3,9 @@ import de.firemage.autograder.core.LocalizedMessage; import de.firemage.autograder.core.ProblemType; import de.firemage.autograder.core.check.ExecutableCheck; +import de.firemage.autograder.core.integrated.CoreUtil; import de.firemage.autograder.core.integrated.IntegratedCheck; -import de.firemage.autograder.core.integrated.SpoonUtil; +import de.firemage.autograder.core.integrated.StatementUtil; import de.firemage.autograder.core.integrated.StaticAnalysis; import spoon.processing.AbstractProcessor; import spoon.reflect.code.CtStatement; @@ -59,8 +60,8 @@ public void process(CtWhile ctWhile) { // } // ``` - Deque loopStatements = new ArrayDeque<>(SpoonUtil.getEffectiveStatements(ctWhile.getBody())); - CtStatement currentStatementBeforeLoop = SpoonUtil.getPreviousStatement(ctWhile).orElse(null); + Deque loopStatements = new ArrayDeque<>(StatementUtil.getEffectiveStatements(ctWhile.getBody())); + CtStatement currentStatementBeforeLoop = StatementUtil.getPreviousStatement(ctWhile).orElse(null); if (currentStatementBeforeLoop == null || loopStatements.isEmpty()) { return; } @@ -72,7 +73,7 @@ public void process(CtWhile ctWhile) { return; } - currentStatementBeforeLoop = SpoonUtil.getPreviousStatement(currentStatementBeforeLoop).orElse(null); + currentStatementBeforeLoop = StatementUtil.getPreviousStatement(currentStatementBeforeLoop).orElse(null); } if (loopStatements.isEmpty()) { @@ -81,7 +82,7 @@ public void process(CtWhile ctWhile) { new LocalizedMessage( "loop-should-be-do-while", Map.of("suggestion", """ - %ndo %s while (%s)""".formatted(SpoonUtil.truncatedSuggestion(ctWhile.getBody()), ctWhile.getLoopingExpression())) + %ndo %s while (%s)""".formatted(CoreUtil.truncatedSuggestion(ctWhile.getBody()), ctWhile.getLoopingExpression())) ), ProblemType.LOOP_SHOULD_BE_DO_WHILE ); diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/check/general/LoopShouldBeFor.java b/autograder-core/src/main/java/de/firemage/autograder/core/check/general/LoopShouldBeFor.java index c9a8065c..b5db18e3 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/check/general/LoopShouldBeFor.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/check/general/LoopShouldBeFor.java @@ -4,9 +4,11 @@ import de.firemage.autograder.core.LocalizedMessage; import de.firemage.autograder.core.ProblemType; import de.firemage.autograder.core.check.ExecutableCheck; +import de.firemage.autograder.core.integrated.FactoryUtil; import de.firemage.autograder.core.integrated.IntegratedCheck; -import de.firemage.autograder.core.integrated.SpoonUtil; +import de.firemage.autograder.core.integrated.StatementUtil; import de.firemage.autograder.core.integrated.StaticAnalysis; +import de.firemage.autograder.core.integrated.TypeUtil; import de.firemage.autograder.core.integrated.UsesFinder; import spoon.processing.AbstractProcessor; import spoon.reflect.code.BinaryOperatorKind; @@ -62,15 +64,15 @@ public String toString() { } private static LoopSuggestion getCounter(CtLoop ctLoop, CodeModel model) { - List statements = SpoonUtil.getEffectiveStatements(ctLoop.getBody()); + List statements = StatementUtil.getEffectiveStatements(ctLoop.getBody()); if (statements.isEmpty()) { return null; } - CtStatement previous = SpoonUtil.getPreviousStatement(ctLoop).orElse(null); - if (!(previous instanceof CtLocalVariable ctLocalVariable) || !SpoonUtil.isPrimitiveNumeric(ctLocalVariable.getType())) { + CtStatement previous = StatementUtil.getPreviousStatement(ctLoop).orElse(null); + if (!(previous instanceof CtLocalVariable ctLocalVariable) || !TypeUtil.isPrimitiveNumeric(ctLocalVariable.getType())) { return null; } @@ -129,7 +131,7 @@ private static LoopSuggestion getCounter(CtLoop ctLoop, CodeModel model) { newBody = ctLoop.getFactory().createBlock(); } - boolean isUsedAfterLoop = SpoonUtil.getNextStatements(ctLoop) + boolean isUsedAfterLoop = StatementUtil.getNextStatements(ctLoop) .stream() .anyMatch(statement -> UsesFinder.variableUses(ctLocalVariable).nestedIn(statement).hasAny()); @@ -169,7 +171,7 @@ private static LoopSuggestion getCounter(CtLoop ctLoop, CodeModel model) { upperBound = factory.createInvocation(ctForEach.getExpression().clone(), methods.get(0).getReference(), List.of()); } - condition = SpoonUtil.createBinaryOperator( + condition = FactoryUtil.createBinaryOperator( factory.createVariableRead(ctLocalVariable.getReference(), false), upperBound, BinaryOperatorKind.LT diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/check/general/MagicString.java b/autograder-core/src/main/java/de/firemage/autograder/core/check/general/MagicString.java index c945cd4c..683ac723 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/check/general/MagicString.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/check/general/MagicString.java @@ -4,8 +4,8 @@ import de.firemage.autograder.core.ProblemType; import de.firemage.autograder.core.check.ExecutableCheck; import de.firemage.autograder.core.integrated.IntegratedCheck; -import de.firemage.autograder.core.integrated.SpoonUtil; import de.firemage.autograder.core.integrated.StaticAnalysis; +import de.firemage.autograder.core.integrated.TypeUtil; import spoon.processing.AbstractProcessor; import spoon.reflect.code.CtLiteral; import spoon.reflect.declaration.CtField; @@ -53,7 +53,7 @@ public void process(CtType ctType) { List> magicStrings = ctType.getElements(new CompositeFilter<>( FilteringOperator.INTERSECTION, new TypeFilter<>(CtLiteral.class), - element -> element.getType() != null && SpoonUtil.isTypeEqualTo(element.getType(), String.class), + element -> element.getType() != null && TypeUtil.isTypeEqualTo(element.getType(), String.class), IS_MAGIC_STRING )); diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/check/general/ObjectDatatype.java b/autograder-core/src/main/java/de/firemage/autograder/core/check/general/ObjectDatatype.java index 9b431335..ee5629c4 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/check/general/ObjectDatatype.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/check/general/ObjectDatatype.java @@ -4,8 +4,9 @@ import de.firemage.autograder.core.ProblemType; import de.firemage.autograder.core.check.ExecutableCheck; import de.firemage.autograder.core.integrated.IntegratedCheck; -import de.firemage.autograder.core.integrated.SpoonUtil; import de.firemage.autograder.core.integrated.StaticAnalysis; +import de.firemage.autograder.core.integrated.MethodUtil; +import de.firemage.autograder.core.integrated.TypeUtil; import spoon.processing.AbstractProcessor; import spoon.reflect.declaration.CtVariable; import spoon.reflect.reference.CtTypeReference; @@ -16,7 +17,7 @@ @ExecutableCheck(reportedProblems = { ProblemType.OBJECT_DATATYPE }) public class ObjectDatatype extends IntegratedCheck { private static boolean hasObjectType(CtTypeReference ctTypeReference) { - return !ctTypeReference.isGenerics() && SpoonUtil.isTypeEqualTo(ctTypeReference, java.lang.Object.class) + return !ctTypeReference.isGenerics() && TypeUtil.isTypeEqualTo(ctTypeReference, java.lang.Object.class) || ctTypeReference.getActualTypeArguments().stream().anyMatch(ObjectDatatype::hasObjectType); } @@ -29,7 +30,7 @@ public void process(CtVariable ctVariable) { return; } - if (SpoonUtil.isInOverridingMethod(ctVariable) || ctVariable.getType().isArray()) { + if (MethodUtil.isInOverridingMethod(ctVariable) || ctVariable.getType().isArray()) { return; } diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/check/general/ReassignedParameterCheck.java b/autograder-core/src/main/java/de/firemage/autograder/core/check/general/ReassignedParameterCheck.java index 10d2dc85..4e734ed4 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/check/general/ReassignedParameterCheck.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/check/general/ReassignedParameterCheck.java @@ -4,7 +4,7 @@ import de.firemage.autograder.core.ProblemType; import de.firemage.autograder.core.check.ExecutableCheck; import de.firemage.autograder.core.integrated.IntegratedCheck; -import de.firemage.autograder.core.integrated.SpoonUtil; +import de.firemage.autograder.core.integrated.VariableUtil; import de.firemage.autograder.core.integrated.StaticAnalysis; import spoon.processing.AbstractProcessor; import spoon.reflect.declaration.CtConstructor; @@ -26,7 +26,7 @@ public void process(CtParameter ctParameter) { return; } - if (!SpoonUtil.isEffectivelyFinal(ctParameter)) { + if (!VariableUtil.isEffectivelyFinal(ctParameter)) { addLocalProblem( ctParameter, new LocalizedMessage( diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/check/general/StringCompareCheck.java b/autograder-core/src/main/java/de/firemage/autograder/core/check/general/StringCompareCheck.java index 9797a971..56f1ee98 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/check/general/StringCompareCheck.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/check/general/StringCompareCheck.java @@ -3,9 +3,10 @@ import de.firemage.autograder.core.LocalizedMessage; import de.firemage.autograder.core.ProblemType; import de.firemage.autograder.core.check.ExecutableCheck; +import de.firemage.autograder.core.integrated.ExpressionUtil; import de.firemage.autograder.core.integrated.IntegratedCheck; -import de.firemage.autograder.core.integrated.SpoonUtil; import de.firemage.autograder.core.integrated.StaticAnalysis; +import de.firemage.autograder.core.integrated.TypeUtil; import spoon.processing.AbstractProcessor; import spoon.reflect.code.BinaryOperatorKind; import spoon.reflect.code.CtBinaryOperator; @@ -16,8 +17,8 @@ @ExecutableCheck(reportedProblems = { ProblemType.STRING_COMPARE_BY_REFERENCE }) public class StringCompareCheck extends IntegratedCheck { private static boolean isStringComparison(CtExpression lhs, CtExpression rhs) { - return SpoonUtil.isString(lhs.getType()) && !SpoonUtil.isNullLiteral(rhs) - || SpoonUtil.isString(rhs.getType()) && !SpoonUtil.isNullLiteral(lhs); + return TypeUtil.isString(lhs.getType()) && !ExpressionUtil.isNullLiteral(rhs) + || TypeUtil.isString(rhs.getType()) && !ExpressionUtil.isNullLiteral(lhs); } @Override diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/check/general/UseDifferentVisibility.java b/autograder-core/src/main/java/de/firemage/autograder/core/check/general/UseDifferentVisibility.java index 0d261f89..5c7618ee 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/check/general/UseDifferentVisibility.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/check/general/UseDifferentVisibility.java @@ -4,9 +4,10 @@ import de.firemage.autograder.core.ProblemType; import de.firemage.autograder.core.check.ExecutableCheck; import de.firemage.autograder.core.integrated.IntegratedCheck; +import de.firemage.autograder.core.integrated.ElementUtil; import de.firemage.autograder.core.integrated.MethodHierarchy; -import de.firemage.autograder.core.integrated.SpoonUtil; import de.firemage.autograder.core.integrated.StaticAnalysis; +import de.firemage.autograder.core.integrated.MethodUtil; import de.firemage.autograder.core.integrated.UsesFinder; import spoon.processing.AbstractProcessor; import spoon.reflect.CtModel; @@ -71,7 +72,7 @@ private static Visibility getVisibility(CtTypeMember ctTypeMember) { } List references = referencesStream.toList(); - CtElement commonParent = SpoonUtil.findCommonParent(ctTypeMember, references); + CtElement commonParent = ElementUtil.findCommonParent(ctTypeMember, references); CtType declaringType = ctTypeMember.getDeclaringType(); // if there are no references, the member itself will be returned @@ -112,7 +113,7 @@ public void process(CtTypeMember ctTypeMember) { } Visibility currentVisibility = Visibility.of(ctTypeMember); - if (ctTypeMember instanceof CtMethod ctMethod && (SpoonUtil.isMainMethod(ctMethod) || MethodHierarchy.isOverridingMethod(ctMethod))) { + if (ctTypeMember instanceof CtMethod ctMethod && (MethodUtil.isMainMethod(ctMethod) || MethodHierarchy.isOverridingMethod(ctMethod))) { return; } diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/check/general/UseGuardClauses.java b/autograder-core/src/main/java/de/firemage/autograder/core/check/general/UseGuardClauses.java index fad1850a..10b737d5 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/check/general/UseGuardClauses.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/check/general/UseGuardClauses.java @@ -3,8 +3,9 @@ import de.firemage.autograder.core.LocalizedMessage; import de.firemage.autograder.core.ProblemType; import de.firemage.autograder.core.check.ExecutableCheck; +import de.firemage.autograder.core.integrated.FactoryUtil; import de.firemage.autograder.core.integrated.IntegratedCheck; -import de.firemage.autograder.core.integrated.SpoonUtil; +import de.firemage.autograder.core.integrated.StatementUtil; import de.firemage.autograder.core.integrated.StaticAnalysis; import de.firemage.autograder.core.integrated.effects.Effect; import de.firemage.autograder.core.integrated.effects.TerminalEffect; @@ -29,12 +30,12 @@ private void reportProblem(CtStatement ctStatement, CtExpression condition) { } private boolean isTerminal(CtStatement ctStatement) { - List ctStatements = SpoonUtil.getEffectiveStatements(ctStatement); + List ctStatements = StatementUtil.getEffectiveStatements(ctStatement); if (ctStatements.isEmpty()) return false; - Optional optionalEffect = SpoonUtil.tryMakeEffect(ctStatements.get(ctStatements.size() - 1)); + Optional optionalEffect = StatementUtil.tryMakeEffect(ctStatements.get(ctStatements.size() - 1)); return optionalEffect.map(TerminalEffect.class::isInstance).orElse(false); } @@ -42,7 +43,7 @@ private boolean isTerminal(CtStatement ctStatement) { private void checkCtIf(CtIf ctIf, CtExpression condition) { // if the condition != null, then the ctIf is an else if if (condition != null) { - CtExpression ifCondition = SpoonUtil.createBinaryOperator( + CtExpression ifCondition = FactoryUtil.createBinaryOperator( condition, ctIf.getCondition(), BinaryOperatorKind.AND @@ -54,7 +55,7 @@ private void checkCtIf(CtIf ctIf, CtExpression condition) { } // the condition to reach the else statement - CtExpression elseCondition = SpoonUtil.createUnaryOperator( + CtExpression elseCondition = FactoryUtil.createUnaryOperator( UnaryOperatorKind.NOT, ctIf.getCondition() ); @@ -64,10 +65,10 @@ private void checkCtIf(CtIf ctIf, CtExpression condition) { // if there is no else, return if (ctStatement == null) return; - List ctStatements = SpoonUtil.getEffectiveStatements(ctStatement); + List ctStatements = StatementUtil.getEffectiveStatements(ctStatement); if (condition != null) { - elseCondition = SpoonUtil.createBinaryOperator( + elseCondition = FactoryUtil.createBinaryOperator( condition, elseCondition, BinaryOperatorKind.AND @@ -75,7 +76,7 @@ private void checkCtIf(CtIf ctIf, CtExpression condition) { } if (ctStatements.size() == 1 && ctStatements.get(0) instanceof CtIf ctElseIf) { - CtExpression elseIfCondition = SpoonUtil.createBinaryOperator( + CtExpression elseIfCondition = FactoryUtil.createBinaryOperator( elseCondition, ctElseIf.getCondition(), BinaryOperatorKind.AND @@ -97,7 +98,7 @@ public void process(CtIf ctIf) { CtIf parentIf = ctIf.getParent(CtIf.class); if (parentIf != null && parentIf.getElseStatement() != null) { - List ctStatements = SpoonUtil.getEffectiveStatements(parentIf.getElseStatement()); + List ctStatements = StatementUtil.getEffectiveStatements(parentIf.getElseStatement()); if (ctStatements.size() == 1 && ctStatements.get(0).equals(ctIf)) { return; } diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/check/naming/ConstantsHaveDescriptiveNamesCheck.java b/autograder-core/src/main/java/de/firemage/autograder/core/check/naming/ConstantsHaveDescriptiveNamesCheck.java index 6368155a..8d749d2c 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/check/naming/ConstantsHaveDescriptiveNamesCheck.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/check/naming/ConstantsHaveDescriptiveNamesCheck.java @@ -3,9 +3,11 @@ import de.firemage.autograder.core.LocalizedMessage; import de.firemage.autograder.core.ProblemType; import de.firemage.autograder.core.check.ExecutableCheck; +import de.firemage.autograder.core.integrated.FactoryUtil; import de.firemage.autograder.core.integrated.IntegratedCheck; -import de.firemage.autograder.core.integrated.SpoonUtil; import de.firemage.autograder.core.integrated.StaticAnalysis; +import de.firemage.autograder.core.integrated.MethodUtil; +import de.firemage.autograder.core.integrated.TypeUtil; import spoon.processing.AbstractProcessor; import spoon.reflect.code.CtInvocation; import spoon.reflect.code.CtLiteral; @@ -197,9 +199,9 @@ public void process(CtField field) { } else if (field.getDefaultExpression() instanceof CtInvocation ctInvocation // check if the value is System.lineSeparator() && ctInvocation.getTarget() instanceof CtTypeAccess ctTypeAccess - && SpoonUtil.isTypeEqualTo(ctTypeAccess.getAccessedType(), java.lang.System.class) - && SpoonUtil.isSignatureEqualTo(ctInvocation.getExecutable(), String.class, "lineSeparator")) { - literal = SpoonUtil.makeLiteral(field.getFactory().Type().stringType(), "\n"); + && TypeUtil.isTypeEqualTo(ctTypeAccess.getAccessedType(), java.lang.System.class) + && MethodUtil.isSignatureEqualTo(ctInvocation.getExecutable(), String.class, "lineSeparator")) { + literal = FactoryUtil.makeLiteral(field.getFactory().Type().stringType(), "\n"); } else { return; } diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/check/naming/PackageNamingConvention.java b/autograder-core/src/main/java/de/firemage/autograder/core/check/naming/PackageNamingConvention.java index 86da3ab7..180cbc7b 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/check/naming/PackageNamingConvention.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/check/naming/PackageNamingConvention.java @@ -3,8 +3,8 @@ import de.firemage.autograder.core.LocalizedMessage; import de.firemage.autograder.core.ProblemType; import de.firemage.autograder.core.check.ExecutableCheck; +import de.firemage.autograder.core.integrated.CoreUtil; import de.firemage.autograder.core.integrated.IntegratedCheck; -import de.firemage.autograder.core.integrated.SpoonUtil; import de.firemage.autograder.core.integrated.StaticAnalysis; import spoon.reflect.cu.SourcePosition; import spoon.reflect.declaration.CtCompilationUnit; @@ -88,7 +88,7 @@ protected void check(StaticAnalysis staticAnalysis) { String positions = declarations.stream() .map(CtPackageDeclaration::getPosition) - .map(SpoonUtil::formatSourcePosition) + .map(CoreUtil::formatSourcePosition) .collect(Collectors.joining(", ")); this.addLocalProblem( diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/check/oop/AvoidInnerClasses.java b/autograder-core/src/main/java/de/firemage/autograder/core/check/oop/AvoidInnerClasses.java index 5a879859..6aaf764c 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/check/oop/AvoidInnerClasses.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/check/oop/AvoidInnerClasses.java @@ -4,8 +4,8 @@ import de.firemage.autograder.core.ProblemType; import de.firemage.autograder.core.check.ExecutableCheck; import de.firemage.autograder.core.integrated.IntegratedCheck; -import de.firemage.autograder.core.integrated.SpoonUtil; import de.firemage.autograder.core.integrated.StaticAnalysis; +import de.firemage.autograder.core.integrated.TypeUtil; import spoon.processing.AbstractProcessor; import spoon.reflect.declaration.CtType; @@ -13,7 +13,7 @@ public class AvoidInnerClasses extends IntegratedCheck { private void checkCtType(CtType ctType) { // only lint non-private static inner classes - if (SpoonUtil.isInnerClass(ctType) && !ctType.isPrivate() && (ctType.isStatic() || ctType.isInterface() || ctType.isEnum() || ctType.isLocalType())) { + if (TypeUtil.isInnerClass(ctType) && !ctType.isPrivate() && (ctType.isStatic() || ctType.isInterface() || ctType.isEnum() || ctType.isLocalType())) { this.addLocalProblem( ctType, new LocalizedMessage("avoid-inner-classes"), diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/check/oop/ClosedSetOfValues.java b/autograder-core/src/main/java/de/firemage/autograder/core/check/oop/ClosedSetOfValues.java index d69fa94d..006dbf13 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/check/oop/ClosedSetOfValues.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/check/oop/ClosedSetOfValues.java @@ -3,11 +3,14 @@ import de.firemage.autograder.core.LocalizedMessage; import de.firemage.autograder.core.ProblemType; import de.firemage.autograder.core.check.ExecutableCheck; +import de.firemage.autograder.core.integrated.ExpressionUtil; import de.firemage.autograder.core.integrated.IntegratedCheck; -import de.firemage.autograder.core.integrated.SpoonUtil; +import de.firemage.autograder.core.integrated.VariableUtil; +import de.firemage.autograder.core.integrated.StatementUtil; import de.firemage.autograder.core.integrated.StaticAnalysis; import de.firemage.autograder.core.integrated.effects.AssignmentEffect; import de.firemage.autograder.core.integrated.effects.TerminalEffect; +import de.firemage.autograder.core.integrated.TypeUtil; import spoon.reflect.code.CtAbstractSwitch; import spoon.reflect.code.CtCase; import spoon.reflect.code.CtExpression; @@ -42,7 +45,7 @@ public class ClosedSetOfValues extends IntegratedCheck { ); private static boolean isSupportedType(CtTypeReference ctTypeReference) { - return SpoonUtil.isTypeEqualTo(ctTypeReference, SUPPORTED_TYPES.toArray(new Class[0])); + return TypeUtil.isTypeEqualTo(ctTypeReference, SUPPORTED_TYPES.toArray(new Class[0])); } private boolean shouldSwitchOverEnum(CtAbstractSwitch ctSwitch) { @@ -52,7 +55,7 @@ private boolean shouldSwitchOverEnum(CtAbstractSwitch ctSwitch) { continue; } - var effect = SpoonUtil.getSingleEffect(ctCase.getStatements()).orElse(null); + var effect = StatementUtil.getSingleEffect(ctCase.getStatements()).orElse(null); if (!(effect instanceof TerminalEffect || effect instanceof AssignmentEffect)) { return true; @@ -67,7 +70,7 @@ private static Set> getDistinctElementsFromSwitch(CtAbstractSwitch< List> elements = ctSwitch.getCases() .stream() .flatMap((CtCase e) -> e.getCaseExpressions().stream()) - .map(SpoonUtil::resolveCtExpression) + .map(ExpressionUtil::resolveCtExpression) .toList(); if (elements.stream().anyMatch(e -> !(e instanceof CtLiteral))) { @@ -121,7 +124,7 @@ private static List> getFiniteSet(Iterable> result = new ArrayList<>(); for (CtExpression ctExpression : elements) { - CtExpression resolved = SpoonUtil.resolveCtExpression(ctExpression); + CtExpression resolved = ExpressionUtil.resolveCtExpression(ctExpression); if (!isSupportedType(resolved.getType()) || !(resolved instanceof CtLiteral ctLiteral)) { return List.of(); @@ -215,14 +218,14 @@ public void visitCtField(CtField ctField) { return; } - if (!SpoonUtil.isEffectivelyFinal(ctField)) { + if (!VariableUtil.isEffectivelyFinal(ctField)) { return; } if (ctField.getType().isArray() && ctExpression instanceof CtNewArray ctNewArray) { checkFiniteListing(ctExpression, ctNewArray.getElements()); } else { - checkFiniteListing(ctExpression, SpoonUtil.getElementsOfExpression(ctExpression)); + checkFiniteListing(ctExpression, ExpressionUtil.getElementsOfExpression(ctExpression)); } super.visitCtField(ctField); @@ -239,14 +242,14 @@ public void visitCtLocalVariable(CtLocalVariable ctLocalVariable) { return; } - if (!SpoonUtil.isEffectivelyFinal(ctLocalVariable)) { + if (!VariableUtil.isEffectivelyFinal(ctLocalVariable)) { return; } if (ctLocalVariable.getType().isArray() && ctExpression instanceof CtNewArray ctNewArray) { checkFiniteListing(ctExpression, ctNewArray.getElements()); } else { - checkFiniteListing(ctExpression, SpoonUtil.getElementsOfExpression(ctExpression)); + checkFiniteListing(ctExpression, ExpressionUtil.getElementsOfExpression(ctExpression)); } super.visitCtLocalVariable(ctLocalVariable); diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/check/oop/ConcreteCollectionCheck.java b/autograder-core/src/main/java/de/firemage/autograder/core/check/oop/ConcreteCollectionCheck.java index f1c8a98d..d9458a0e 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/check/oop/ConcreteCollectionCheck.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/check/oop/ConcreteCollectionCheck.java @@ -4,8 +4,9 @@ import de.firemage.autograder.core.ProblemType; import de.firemage.autograder.core.check.ExecutableCheck; import de.firemage.autograder.core.integrated.IntegratedCheck; -import de.firemage.autograder.core.integrated.SpoonUtil; import de.firemage.autograder.core.integrated.StaticAnalysis; +import de.firemage.autograder.core.integrated.MethodUtil; +import de.firemage.autograder.core.integrated.TypeUtil; import spoon.reflect.code.BinaryOperatorKind; import spoon.reflect.code.CtArrayAccess; import spoon.reflect.code.CtBinaryOperator; @@ -52,13 +53,11 @@ public class ConcreteCollectionCheck extends IntegratedCheck { private boolean isConcreteCollectionType(CtTypeReference ctType) { return !ctType.isInterface() && Stream.of(java.util.Collection.class, java.util.Map.class) - .anyMatch(ctClass -> SpoonUtil.isSubtypeOf(ctType, ctClass)); + .anyMatch(ctClass -> TypeUtil.isSubtypeOf(ctType, ctClass)); } private boolean isAllowedType(CtTypeReference ctTypeReference) { - return ALLOWED_TYPES.stream() - .map(ty -> ctTypeReference.getFactory().Type().createReference(ty, false)) - .anyMatch(ctTypeReference::equals); + return TypeUtil.isTypeEqualTo(ctTypeReference, ALLOWED_TYPES); } private boolean isInAllowedContext(CtTypeReference ctTypeReference) { @@ -113,9 +112,7 @@ private boolean isInAllowedContext(CtTypeReference ctTypeReference) { if (ctFieldRead != null) { CtFieldReference ctFieldReference = ctFieldRead.getVariable(); return ctFieldReference.getDeclaringType().equals(ctTypeReference) - // TODO: use SpoonUtil.isTypeEqualTo - && ctFieldReference.getType() - .equals(ctTypeReference.getFactory().Type().createReference(Class.class, false)); + && TypeUtil.isTypeEqualTo(ctFieldReference.getType(), java.lang.Class.class); } // AbstractMap.SimpleEntry @@ -156,7 +153,7 @@ private boolean checkCtTypeReference(CtTypeReference ctTypeReference) { return true; } - if (SpoonUtil.isInOverridingMethod(ctTypeReference) + if (MethodUtil.isInOverridingMethod(ctTypeReference) || this.isInAllowedContext(ctTypeReference) || this.isAllowedType(ctTypeReference)) { return false; diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/check/oop/DoNotMakeConstantsClasses.java b/autograder-core/src/main/java/de/firemage/autograder/core/check/oop/DoNotMakeConstantsClasses.java index 1aa3d944..3cbe9949 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/check/oop/DoNotMakeConstantsClasses.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/check/oop/DoNotMakeConstantsClasses.java @@ -4,8 +4,9 @@ import de.firemage.autograder.core.ProblemType; import de.firemage.autograder.core.check.ExecutableCheck; import de.firemage.autograder.core.integrated.IntegratedCheck; -import de.firemage.autograder.core.integrated.SpoonUtil; +import de.firemage.autograder.core.integrated.VariableUtil; import de.firemage.autograder.core.integrated.StaticAnalysis; +import de.firemage.autograder.core.integrated.TypeUtil; import spoon.processing.AbstractProcessor; import spoon.reflect.code.CtConstructorCall; import spoon.reflect.code.CtExpression; @@ -27,7 +28,7 @@ private boolean isConstantsClassLike(CtType ctType) { && !ctType.getFields().isEmpty() // all fields should be static and effectively final (no assignments) && ctType.getFields().stream().allMatch( - ctField -> ctField.isStatic() && SpoonUtil.isEffectivelyFinal(ctField) + ctField -> ctField.isStatic() && VariableUtil.isEffectivelyFinal(ctField) ) // the class should not be abstract && !ctType.isAbstract() @@ -36,7 +37,7 @@ private boolean isConstantsClassLike(CtType ctType) { // the class should not have any inner classes && ctType.getNestedTypes().isEmpty() // the class itself should not be an inner class - && !SpoonUtil.isInnerClass(ctType); + && !TypeUtil.isInnerClass(ctType); } private boolean isConstantsEnum(CtType ctType) { diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/check/oop/IOUISeparation.java b/autograder-core/src/main/java/de/firemage/autograder/core/check/oop/IOUISeparation.java index 3fc3f849..24087b01 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/check/oop/IOUISeparation.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/check/oop/IOUISeparation.java @@ -3,9 +3,12 @@ import de.firemage.autograder.core.LocalizedMessage; import de.firemage.autograder.core.ProblemType; import de.firemage.autograder.core.check.ExecutableCheck; +import de.firemage.autograder.core.integrated.CoreUtil; import de.firemage.autograder.core.integrated.IntegratedCheck; -import de.firemage.autograder.core.integrated.SpoonUtil; import de.firemage.autograder.core.integrated.StaticAnalysis; +import de.firemage.autograder.core.integrated.ElementUtil; +import de.firemage.autograder.core.integrated.MethodUtil; +import de.firemage.autograder.core.integrated.TypeUtil; import spoon.reflect.CtModel; import spoon.reflect.code.CtFieldRead; import spoon.reflect.code.CtInvocation; @@ -36,7 +39,7 @@ private boolean hasAccessedSystem(CtInvocation ctInvocation) { && ctFieldRead.getVariable().isStatic() && ctFieldRead.getVariable().isFinal() // check that the Attribute is accessed from the class System - && SpoonUtil.isTypeEqualTo(ctTypeAccess.getAccessedType(), System.class); + && TypeUtil.isTypeEqualTo(ctTypeAccess.getAccessedType(), System.class); } /** @@ -48,7 +51,7 @@ private boolean hasAccessedSystem(CtInvocation ctInvocation) { private boolean hasAccessedScanner(CtInvocation ctInvocation) { return ctInvocation.getTarget() instanceof CtVariableRead ctVariableRead && ctVariableRead.getVariable() != null // just to be sure - && SpoonUtil.isTypeEqualTo(ctVariableRead.getVariable().getType(), java.util.Scanner.class); + && TypeUtil.isTypeEqualTo(ctVariableRead.getVariable().getType(), java.util.Scanner.class); } private static boolean isAllowedLocation(boolean requireSameClass, List uses) { @@ -57,7 +60,7 @@ private static boolean isAllowedLocation(boolean requireSameClass, List ctTypedElement) { - return ctTypedElement.getType().isArray() || SpoonUtil.isSubtypeOf(ctTypedElement.getType(), java.util.Collection.class); + return ctTypedElement.getType().isArray() || TypeUtil.isSubtypeOf(ctTypedElement.getType(), java.util.Collection.class); } /** @@ -93,7 +96,7 @@ private static boolean canBeMutated(CtField ctVariable) { return true; } - if (!SpoonUtil.isSubtypeOf(ctVariable.getType(), java.util.Collection.class)) { + if (!TypeUtil.isSubtypeOf(ctVariable.getType(), java.util.Collection.class)) { // not a collection return false; } @@ -125,7 +128,7 @@ private static boolean isMutableExpression(CtExpression ctExpression) { } // we only care about arrays and collections for now - if (!SpoonUtil.isSubtypeOf(ctExpression.getType(), java.util.Collection.class)) { + if (!TypeUtil.isSubtypeOf(ctExpression.getType(), java.util.Collection.class)) { // not a collection return false; } @@ -156,7 +159,7 @@ private static boolean isMutableExpression(CtExpression ctExpression) { // In both cases the assigned expression is mutable, and we have to detect this. // To do this, we check if the assigned variable is mutable. if (ctExpression instanceof CtVariableRead ctVariableRead && ctExecutable != null) { - CtVariable ctVariable = SpoonUtil.getVariableDeclaration(ctVariableRead.getVariable()); + CtVariable ctVariable = VariableUtil.getVariableDeclaration(ctVariableRead.getVariable()); // this is a special case for enums, where the constructor is private and mutability can only be introduced // through the enum constructor calls @@ -164,7 +167,7 @@ private static boolean isMutableExpression(CtExpression ctExpression) { && ctConstructor.getDeclaringType() instanceof CtEnum ctEnum && hasAssignedParameterReference(ctExpression, ctConstructor)) { // figure out the index of the parameter reference: - CtParameter ctParameterToFind = findParameterReference(ctExpression, ctConstructor).unwrap(); + CtParameter ctParameterToFind = findParameterReference(ctExpression, ctConstructor).orElseThrow(); int index = -1; for (CtParameter ctParameter : ctConstructor.getParameters()) { index += 1; @@ -211,7 +214,7 @@ private static List> findPreviousAssignee(CtVariableRead ctVa boolean foundPreviousAssignment = false; CtStatement currentStatement = ctVariableRead.getParent(CtStatement.class); - var reversedStatements = new ArrayList<>(SpoonUtil.getEffectiveStatements(ctExecutable.getBody())); + var reversedStatements = new ArrayList<>(StatementUtil.getEffectiveStatements(ctExecutable.getBody())); Collections.reverse(reversedStatements); for (CtStatement ctStatement : reversedStatements) { if (!foundPreviousAssignment) { @@ -238,7 +241,7 @@ private static Option> findParameterReference(CtExpression ctE return Option.none(); } - CtVariable ctVariableDeclaration = SpoonUtil.getVariableDeclaration(ctVariableRead.getVariable()); + CtVariable ctVariableDeclaration = VariableUtil.getVariableDeclaration(ctVariableRead.getVariable()); if (ctVariableDeclaration != null && isParameterOf(ctVariableDeclaration, ctExecutable)) { // There is a special-case: one can reassign the parameter to itself with a different value: @@ -266,7 +269,7 @@ private static boolean hasAssignedParameterReference(CtExpression ctExpressio } private void checkCtExecutableReturn(CtExecutable ctExecutable) { - List statements = SpoonUtil.getEffectiveStatements(ctExecutable.getBody()); + List statements = StatementUtil.getEffectiveStatements(ctExecutable.getBody()); // a lambda like () -> true does not have a body, but an expression which is a return statement // this case is handled here @@ -303,7 +306,7 @@ private void checkCtExecutableReturn(CtExecutable ctExecutable) { if (canBeMutated(field)) { addLocalProblem( - SpoonUtil.findValidPosition(ctExecutable), + ElementUtil.findValidPosition(ctExecutable), new LocalizedMessage( "leaked-collection-return", Map.of( @@ -338,7 +341,7 @@ private void checkCtExecutableAssign(CtExecutable ctExecutable) { return; } - for (CtStatement ctStatement : SpoonUtil.getEffectiveStatements(ctExecutable.getBody())) { + for (CtStatement ctStatement : StatementUtil.getEffectiveStatements(ctExecutable.getBody())) { if (!(ctStatement instanceof CtAssignment ctAssignment) || !(ctAssignment.getAssigned() instanceof CtFieldWrite ctFieldWrite)) { continue; @@ -352,7 +355,7 @@ private void checkCtExecutableAssign(CtExecutable ctExecutable) { && isMutableType(ctField)) { if (ctExecutable instanceof CtConstructor ctConstructor) { addLocalProblem( - SpoonUtil.findValidPosition(ctStatement), + ElementUtil.findValidPosition(ctStatement), new LocalizedMessage( "leaked-collection-constructor", Map.of( @@ -364,7 +367,7 @@ && isMutableType(ctField)) { ); } else { addLocalProblem( - SpoonUtil.findValidPosition(ctStatement), + ElementUtil.findValidPosition(ctStatement), new LocalizedMessage( "leaked-collection-assign", Map.of( diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/check/oop/MethodShouldBeAbstractCheck.java b/autograder-core/src/main/java/de/firemage/autograder/core/check/oop/MethodShouldBeAbstractCheck.java index c28aa230..d8be7d4d 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/check/oop/MethodShouldBeAbstractCheck.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/check/oop/MethodShouldBeAbstractCheck.java @@ -4,7 +4,7 @@ import de.firemage.autograder.core.ProblemType; import de.firemage.autograder.core.check.ExecutableCheck; import de.firemage.autograder.core.integrated.IntegratedCheck; -import de.firemage.autograder.core.integrated.SpoonUtil; +import de.firemage.autograder.core.integrated.StatementUtil; import de.firemage.autograder.core.integrated.StaticAnalysis; import spoon.processing.AbstractProcessor; import spoon.reflect.code.CtConstructorCall; @@ -46,7 +46,7 @@ public void process(CtClass clazz) { continue; } - List statements = SpoonUtil.getEffectiveStatements(method.getBody()); + List statements = StatementUtil.getEffectiveStatements(method.getBody()); if (statements.isEmpty()) { addLocalProblem(method, formatExplanation(method), ProblemType.METHOD_USES_PLACEHOLDER_IMPLEMENTATION); diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/check/oop/MutableEnum.java b/autograder-core/src/main/java/de/firemage/autograder/core/check/oop/MutableEnum.java index 4babbb93..283c1298 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/check/oop/MutableEnum.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/check/oop/MutableEnum.java @@ -4,7 +4,7 @@ import de.firemage.autograder.core.ProblemType; import de.firemage.autograder.core.check.ExecutableCheck; import de.firemage.autograder.core.integrated.IntegratedCheck; -import de.firemage.autograder.core.integrated.SpoonUtil; +import de.firemage.autograder.core.integrated.VariableUtil; import de.firemage.autograder.core.integrated.StaticAnalysis; import spoon.processing.AbstractProcessor; import spoon.reflect.declaration.CtEnum; @@ -22,7 +22,7 @@ public class MutableEnum extends IntegratedCheck { */ private static boolean isMutable(CtType ctType) { for (CtField ctField : ctType.getFields()) { - if (!SpoonUtil.isEffectivelyFinal(ctField)) { + if (!VariableUtil.isEffectivelyFinal(ctField)) { return true; } } diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/check/oop/ShouldBeEnumAttribute.java b/autograder-core/src/main/java/de/firemage/autograder/core/check/oop/ShouldBeEnumAttribute.java index 5b9ace9f..cb4d8d76 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/check/oop/ShouldBeEnumAttribute.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/check/oop/ShouldBeEnumAttribute.java @@ -4,7 +4,7 @@ import de.firemage.autograder.core.ProblemType; import de.firemage.autograder.core.check.ExecutableCheck; import de.firemage.autograder.core.integrated.IntegratedCheck; -import de.firemage.autograder.core.integrated.SpoonUtil; +import de.firemage.autograder.core.integrated.StatementUtil; import de.firemage.autograder.core.integrated.StaticAnalysis; import de.firemage.autograder.core.integrated.effects.Effect; import spoon.processing.AbstractProcessor; @@ -27,7 +27,7 @@ public void process(CtAbstractSwitch ctSwitch) { return; } - List effects = SpoonUtil.getCasesEffects(ctSwitch.getCases()); + List effects = StatementUtil.getCasesEffects(ctSwitch.getCases()); if (effects.isEmpty()) { return; } diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/check/oop/StaticFieldShouldBeInstanceCheck.java b/autograder-core/src/main/java/de/firemage/autograder/core/check/oop/StaticFieldShouldBeInstanceCheck.java index 80a20059..00738c9b 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/check/oop/StaticFieldShouldBeInstanceCheck.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/check/oop/StaticFieldShouldBeInstanceCheck.java @@ -4,8 +4,9 @@ import de.firemage.autograder.core.ProblemType; import de.firemage.autograder.core.check.ExecutableCheck; import de.firemage.autograder.core.integrated.IntegratedCheck; -import de.firemage.autograder.core.integrated.SpoonUtil; +import de.firemage.autograder.core.integrated.VariableUtil; import de.firemage.autograder.core.integrated.StaticAnalysis; +import de.firemage.autograder.core.integrated.TypeUtil; import spoon.processing.AbstractProcessor; import spoon.reflect.declaration.CtField; @@ -28,13 +29,13 @@ public void process(CtField ctField) { // class Foo { static int counter = 0; Foo() { counter++; } } the field counter **must** be static // to keep the code simple, we ignore all fields that are a number: - if (ctField.getType() != null && SpoonUtil.isTypeEqualTo(ctField.getType().unbox(), int.class)) { + if (ctField.getType() != null && TypeUtil.isTypeEqualTo(ctField.getType().unbox(), int.class)) { return; } // the field is not marked as final, so values can be assigned to it. // if the field is assigned multiple times, it should not be static - if (!SpoonUtil.isEffectivelyFinal(ctField)) { + if (!VariableUtil.isEffectivelyFinal(ctField)) { addLocalProblem( ctField, new LocalizedMessage( diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/check/oop/UtilityClassCheck.java b/autograder-core/src/main/java/de/firemage/autograder/core/check/oop/UtilityClassCheck.java index fa8b8987..a801445b 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/check/oop/UtilityClassCheck.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/check/oop/UtilityClassCheck.java @@ -4,8 +4,8 @@ import de.firemage.autograder.core.ProblemType; import de.firemage.autograder.core.check.ExecutableCheck; import de.firemage.autograder.core.integrated.IntegratedCheck; -import de.firemage.autograder.core.integrated.SpoonUtil; import de.firemage.autograder.core.integrated.StaticAnalysis; +import de.firemage.autograder.core.integrated.TypeUtil; import spoon.processing.AbstractProcessor; import spoon.reflect.declaration.CtClass; import spoon.reflect.declaration.CtConstructor; @@ -34,7 +34,7 @@ public static boolean isUtilityClass(CtClass ctClass) { // the class should not implement anything && ctClass.getSuperInterfaces().isEmpty() // the class itself should not be an inner class - && !SpoonUtil.isInnerClass(ctClass); + && !TypeUtil.isInnerClass(ctClass); } private void checkCtClassConstructor(CtClass ctClass, ProblemType problemType) { diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/check/structure/DefaultPackageCheck.java b/autograder-core/src/main/java/de/firemage/autograder/core/check/structure/DefaultPackageCheck.java index 271d632a..89a3b658 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/check/structure/DefaultPackageCheck.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/check/structure/DefaultPackageCheck.java @@ -3,8 +3,8 @@ import de.firemage.autograder.core.LocalizedMessage; import de.firemage.autograder.core.ProblemType; import de.firemage.autograder.core.check.ExecutableCheck; +import de.firemage.autograder.core.integrated.CoreUtil; import de.firemage.autograder.core.integrated.IntegratedCheck; -import de.firemage.autograder.core.integrated.SpoonUtil; import de.firemage.autograder.core.integrated.StaticAnalysis; import spoon.reflect.declaration.CtPackage; import spoon.reflect.declaration.CtType; @@ -34,7 +34,7 @@ protected void check(StaticAnalysis staticAnalysis) { String positions = typesInDefaultPackage.stream() .map(CtType::getPosition) - .map(SpoonUtil::formatSourcePosition) + .map(CoreUtil::formatSourcePosition) .collect(Collectors.joining(", ")); this.addLocalProblem( diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/check/structure/DuplicateCode.java b/autograder-core/src/main/java/de/firemage/autograder/core/check/structure/DuplicateCode.java index 0891b8d4..e0a322f7 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/check/structure/DuplicateCode.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/check/structure/DuplicateCode.java @@ -3,8 +3,9 @@ import de.firemage.autograder.core.LocalizedMessage; import de.firemage.autograder.core.ProblemType; import de.firemage.autograder.core.check.ExecutableCheck; +import de.firemage.autograder.core.integrated.CoreUtil; import de.firemage.autograder.core.integrated.IntegratedCheck; -import de.firemage.autograder.core.integrated.SpoonUtil; +import de.firemage.autograder.core.integrated.StatementUtil; import de.firemage.autograder.core.integrated.StaticAnalysis; import de.firemage.autograder.core.integrated.UsesFinder; import de.firemage.autograder.core.integrated.structure.StructuralElement; @@ -48,7 +49,7 @@ private static String formatSourceRange(CtElement start, CtElement end) { return String.format( "%s:%d-%d", - SpoonUtil.getBaseName(startPosition.getFile().getName()), + CoreUtil.getBaseName(startPosition.getFile().getName()), startPosition.getLine(), endPosition.getEndLine() ); @@ -131,7 +132,7 @@ public int countExposedVariables() { } int count = 0; - for (CtStatement ctStatement : SpoonUtil.getNextStatements(this.getLast())) { + for (CtStatement ctStatement : StatementUtil.getNextStatements(this.getLast())) { for (CtVariable declaredVariable : declaredVariables) { if (UsesFinder.variableUses(declaredVariable).nestedIn(ctStatement).hasAny()) { count += 1; @@ -206,7 +207,7 @@ private void checkCtStatement(CtStatement ctStatement) { CodeSegment leftCode = CodeSegment.of(ctStatement); CodeSegment rightCode = CodeSegment.of(duplicate); - for (var entry : zip(SpoonUtil.getNextStatements(ctStatement), SpoonUtil.getNextStatements(duplicate))) { + for (var entry : zip(StatementUtil.getNextStatements(ctStatement), StatementUtil.getNextStatements(duplicate))) { if (!StructuralEqualsVisitor.equals(entry.getKey(), entry.getValue())) { break; } @@ -271,7 +272,7 @@ public void visitCtMethod(CtMethod ctMethod) { return; } - for (CtStatement ctStatement : SpoonUtil.getEffectiveStatements(ctMethod.getBody())) { + for (CtStatement ctStatement : StatementUtil.getEffectiveStatements(ctMethod.getBody())) { this.checkCtStatement(ctStatement); } diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/check/unnecessary/EmptyBlockCheck.java b/autograder-core/src/main/java/de/firemage/autograder/core/check/unnecessary/EmptyBlockCheck.java index 7db7f0ce..b58b39f3 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/check/unnecessary/EmptyBlockCheck.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/check/unnecessary/EmptyBlockCheck.java @@ -4,8 +4,9 @@ import de.firemage.autograder.core.ProblemType; import de.firemage.autograder.core.check.ExecutableCheck; import de.firemage.autograder.core.integrated.IntegratedCheck; -import de.firemage.autograder.core.integrated.SpoonUtil; +import de.firemage.autograder.core.integrated.StatementUtil; import de.firemage.autograder.core.integrated.StaticAnalysis; +import de.firemage.autograder.core.integrated.MethodUtil; import spoon.reflect.code.CtBlock; import spoon.reflect.code.CtCatch; import spoon.reflect.code.CtComment; @@ -16,7 +17,7 @@ @ExecutableCheck(reportedProblems = {ProblemType.EMPTY_BLOCK, ProblemType.EMPTY_CATCH}) public class EmptyBlockCheck extends IntegratedCheck { private static boolean isEmptyBlock(CtBlock ctBlock) { - return SpoonUtil.getEffectiveStatements(ctBlock).isEmpty() + return StatementUtil.getEffectiveStatements(ctBlock).isEmpty() // allow empty blocks that only contain comments && (ctBlock.getStatements().isEmpty() || !ctBlock.getStatements().stream().allMatch(CtComment.class::isInstance)); } @@ -33,7 +34,7 @@ public void visitCtBlock(CtBlock ctBlock) { if (ctBlock.getParent() instanceof CtMethod ctMethod && ctMethod.getBody().equals(ctBlock) - && SpoonUtil.isInOverridingMethod(ctBlock)) { + && MethodUtil.isInOverridingMethod(ctBlock)) { return; } diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/check/unnecessary/UnusedCodeElementCheck.java b/autograder-core/src/main/java/de/firemage/autograder/core/check/unnecessary/UnusedCodeElementCheck.java index 5911800e..e928b801 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/check/unnecessary/UnusedCodeElementCheck.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/check/unnecessary/UnusedCodeElementCheck.java @@ -4,10 +4,12 @@ import de.firemage.autograder.core.LocalizedMessage; import de.firemage.autograder.core.ProblemType; import de.firemage.autograder.core.check.ExecutableCheck; +import de.firemage.autograder.core.integrated.ElementUtil; import de.firemage.autograder.core.integrated.IntegratedCheck; import de.firemage.autograder.core.integrated.MethodHierarchy; -import de.firemage.autograder.core.integrated.SpoonUtil; import de.firemage.autograder.core.integrated.StaticAnalysis; +import de.firemage.autograder.core.integrated.MethodUtil; +import de.firemage.autograder.core.integrated.TypeUtil; import de.firemage.autograder.core.integrated.UsesFinder; import spoon.reflect.code.CtLambda; import spoon.reflect.code.CtLocalVariable; @@ -40,8 +42,8 @@ public class UnusedCodeElementCheck extends IntegratedCheck { */ public static boolean isConsideredUnused(CtNamedElement element, CodeModel model) { // ignore exception constructors and params in those constructors - var parentConstructor = SpoonUtil.getParentOrSelf(element, CtConstructor.class); - if (parentConstructor != null && SpoonUtil.isSubtypeOf(parentConstructor.getType(), java.lang.Throwable.class)) { + var parentConstructor = ElementUtil.getParentOrSelf(element, CtConstructor.class); + if (parentConstructor != null && TypeUtil.isSubtypeOf(parentConstructor.getType(), java.lang.Throwable.class)) { return false; } @@ -63,7 +65,7 @@ public static boolean isConsideredUnused(CtNamedElement element, CodeModel model return false; } else if (variable instanceof CtParameter parameter && parameter.getParent() instanceof CtMethod method) { // For method parameters, also look in overriding methods - int parameterIndex = SpoonUtil.getParameterIndex(parameter, method); + int parameterIndex = ElementUtil.getParameterIndex(parameter, method); return MethodHierarchy .streamAllOverridingMethods(method) .allMatch(m -> isConsideredUnused(m.getExecutable().getParameters().get(parameterIndex), model)); @@ -132,7 +134,7 @@ public void visitCtLocalVariable(CtLocalVariable ctLocalVariable) { @Override public void visitCtMethod(CtMethod ctMethod) { if (MethodHierarchy.isOverridingMethod(ctMethod) - || SpoonUtil.isMainMethod(ctMethod)) { + || MethodUtil.isMainMethod(ctMethod)) { super.visitCtMethod(ctMethod); return; } @@ -154,8 +156,8 @@ public void visitCtConstructor(CtConstructor ctConstructor) { @Override public void visitCtParameter(CtParameter ctParameter) { - if (SpoonUtil.isInOverridingMethod(ctParameter) - || SpoonUtil.isInMainMethod(ctParameter) + if (MethodUtil.isInOverridingMethod(ctParameter) + || MethodUtil.isInMainMethod(ctParameter) || ctParameter.getParent() instanceof CtLambda || ctParameter.getParent(CtInterface.class) != null) { super.visitCtParameter(ctParameter); diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/check/utils/Option.java b/autograder-core/src/main/java/de/firemage/autograder/core/check/utils/Option.java index b3c20d12..ba0598cf 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/check/utils/Option.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/check/utils/Option.java @@ -19,7 +19,7 @@ static Option none() { return new None<>(); } - default T unwrap() { + default T orElseThrow() { if (this instanceof Some someValue) { return someValue.value; } else if (this instanceof None) { @@ -33,12 +33,12 @@ default boolean isSome() { } default Option map(Function function) { - if (this instanceof Some someValue) { + if (this instanceof Some someValue) { return new Some<>(function.apply(someValue.value)); } else if (this instanceof None) { return new None<>(); } - throw new IllegalArgumentException(); + throw new IllegalStateException(); } /** @@ -52,7 +52,7 @@ default T nullable() { } else if (this instanceof None) { return null; } - throw new IllegalArgumentException(); + throw new IllegalStateException(); } default Stream stream() { @@ -61,7 +61,7 @@ default Stream stream() { } else if (this instanceof None) { return Stream.empty(); } - throw new IllegalArgumentException(); + throw new IllegalStateException(); } @Override diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/compiler/CompilationDiagnostic.java b/autograder-core/src/main/java/de/firemage/autograder/core/compiler/CompilationDiagnostic.java index 827846d3..1b08f722 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/compiler/CompilationDiagnostic.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/compiler/CompilationDiagnostic.java @@ -1,8 +1,8 @@ package de.firemage.autograder.core.compiler; +import de.firemage.autograder.core.file.SourcePath; import de.firemage.autograder.core.CodePosition; import de.firemage.autograder.core.file.SourceInfo; -import de.firemage.autograder.core.file.SourcePath; import javax.tools.Diagnostic; import javax.tools.JavaFileObject; diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/compiler/PhysicalFileObject.java b/autograder-core/src/main/java/de/firemage/autograder/core/compiler/PhysicalFileObject.java index 2f9cd52c..89e8f79e 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/compiler/PhysicalFileObject.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/compiler/PhysicalFileObject.java @@ -1,8 +1,8 @@ package de.firemage.autograder.core.compiler; -import de.firemage.autograder.core.SerializableCharset; import de.firemage.autograder.core.file.CompilationUnit; import de.firemage.autograder.core.file.SourcePath; +import de.firemage.autograder.core.SerializableCharset; import javax.lang.model.element.Modifier; import javax.lang.model.element.NestingKind; diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/integrated/CoreUtil.java b/autograder-core/src/main/java/de/firemage/autograder/core/integrated/CoreUtil.java new file mode 100644 index 00000000..2ac6a04a --- /dev/null +++ b/autograder-core/src/main/java/de/firemage/autograder/core/integrated/CoreUtil.java @@ -0,0 +1,148 @@ +package de.firemage.autograder.core.integrated; + +import org.apache.commons.io.FilenameUtils; +import spoon.reflect.CtModel; +import spoon.reflect.cu.SourcePosition; +import spoon.reflect.cu.position.CompoundSourcePosition; +import spoon.reflect.declaration.CtCompilationUnit; +import spoon.reflect.declaration.CtElement; +import spoon.reflect.declaration.CtNamedElement; +import spoon.reflect.declaration.CtType; + +import java.io.File; +import java.util.Arrays; +import java.util.Optional; +import java.util.StringJoiner; +import java.util.function.Consumer; + +/** + * Utility class for functionality that does not fit in any other utility class. + */ +public final class CoreUtil { + private static Optional AUTOGRADER_DEBUG_ENVIRONMENT = parseOptionalFlag(System.getenv("AUTOGRADER_DEBUG")); + private static final boolean IS_IN_JUNIT_TEST = Arrays.stream(Thread.currentThread().getStackTrace()) + .anyMatch(element -> element.getClassName().startsWith("org.junit.")); + + private CoreUtil() { + } + + private static Optional parseOptionalFlag(String flag) { + if (flag == null) { + return Optional.empty(); + } + + try { + return Optional.of(Integer.parseInt(flag) != 0); + } catch (NumberFormatException exception) { + return Optional.of(Boolean.parseBoolean(flag)); + } + } + + /** + * Enables debug mode for the autograder. + *
+ * Note that this slows down the execution by a lot and should therefore only be used for debugging purposes. + */ + public static void setDebugMode() { + AUTOGRADER_DEBUG_ENVIRONMENT = Optional.of(true); + } + + /** + * Checks if the code is currently running in debug mode. + *
+ * This is the case if the code is executed in a junit test or if the debug mode is explicitly enabled. + * + * @return {@code true} if the code is currently running in debug mode + */ + public static boolean isInDebugMode() { + return IS_IN_JUNIT_TEST || AUTOGRADER_DEBUG_ENVIRONMENT.orElse(false); + } + + public static void visitCtCompilationUnit(CtModel ctModel, Consumer lambda) { + // it is not possible to visit CtCompilationUnit through the processor API. + // + // in https://github.com/INRIA/spoon/issues/5168 the below code is mentioned as a workaround: + ctModel + .getAllTypes() + .stream() + .map(CtType::getPosition) + .filter(SourcePosition::isValidPosition) + .map(SourcePosition::getCompilationUnit) + // visit each compilation unit only once + .distinct() + .forEach(lambda); + } + + /** + * Converts the provided source position into a human-readable string. + * + * @param sourcePosition the source position as given by spoon + * @return a human-readable string representation of the source position + */ + public static String formatSourcePosition(SourcePosition sourcePosition) { + return String.format("%s:L%d", getBaseName(sourcePosition.getFile().getName()), sourcePosition.getLine()); + } + + public static String getBaseName(String fileName) { + if (fileName == null) { + return null; + } + return FilenameUtils.removeExtension(new File(fileName).getName()); + } + + public static String truncatedSuggestion(CtElement ctElement) { + StringJoiner result = new StringJoiner(System.lineSeparator()); + + for (String line : ctElement.toString().split("\\r?\\n")) { + int newLineLength = 0; + + // this ensures that the truncation is the same on linux and windows + if (!result.toString().contains("\r\n")) { + newLineLength += (int) result.toString().chars().filter(ch -> ch == '\n').count(); + } + + if (result.length() + newLineLength > 150) { + if (line.startsWith(" ")) { + result.add("...".indent(line.length() - line.stripIndent().length()).stripTrailing()); + } else { + result.add("..."); + } + + if (result.toString().startsWith("{")) { + result.add("}"); + } + + break; + } + + result.add(line); + } + + return result.toString(); + } + + public static SourcePosition getNamePosition(CtNamedElement ctNamedElement) { + SourcePosition position = ctNamedElement.getPosition(); + + if (position instanceof CompoundSourcePosition compoundSourcePosition) { + return ctNamedElement.getFactory().createSourcePosition( + position.getCompilationUnit(), + compoundSourcePosition.getNameStart(), + compoundSourcePosition.getNameEnd(), + position.getCompilationUnit().getLineSeparatorPositions() + ); + } + + return position; + } + + public static boolean isInstanceOfAny(Object object, Class... classes) { + for (Class clazz : classes) { + if (clazz.isInstance(object)) { + return true; + } + } + + return false; + } +} diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/integrated/CtElementStream.java b/autograder-core/src/main/java/de/firemage/autograder/core/integrated/CtElementStream.java index dd988e07..a5b42e4d 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/integrated/CtElementStream.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/integrated/CtElementStream.java @@ -144,7 +144,7 @@ public CtElementStream withDirectParent(Class parent) { * @return */ public CtElementStream nestedIn(CtElement parent) { - return this.filter(e -> SpoonUtil.isNestedOrSame(e, parent)); + return this.filter(e -> ElementUtil.isNestedOrSame(e, parent)); } public CtElementStream nestedInAny(CtElement... parents) { @@ -159,7 +159,7 @@ public CtElementStream nestedInAny(Collection parents) { Set potentialParents = Collections.newSetFromMap(new IdentityHashMap<>()); potentialParents.addAll(parents); - return this.filter(e -> SpoonUtil.isAnyNestedOrSame(e, potentialParents)); + return this.filter(e -> ElementUtil.isAnyNestedOrSame(e, potentialParents)); } /** diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/integrated/CtRange.java b/autograder-core/src/main/java/de/firemage/autograder/core/integrated/CtRange.java index 3be16e13..92fb45f4 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/integrated/CtRange.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/integrated/CtRange.java @@ -20,11 +20,11 @@ public static Optional> ofCharRange(CtBinaryOperator @SuppressWarnings("unchecked") private static > Optional> of(CtBinaryOperator ctBinaryOperator, Class... expectedTypes) { Predicate> isLiteral = expr -> expr instanceof CtLiteral ctLiteral - && SpoonUtil.isTypeEqualTo(ctLiteral.getType(), expectedTypes); + && TypeUtil.isTypeEqualTo(ctLiteral.getType(), expectedTypes); // swap operator if necessary, so that the literal is on the right side: // - CtBinaryOperator result = SpoonUtil.normalizeBy( + CtBinaryOperator result = ExpressionUtil.normalizeBy( (left, right) -> isLiteral.test(left) && !isLiteral.test(right), ctBinaryOperator ); @@ -44,17 +44,17 @@ private static > Optional> of(CtBinaryOperato public Range toRange() { // if (this.operator == BinaryOperatorKind.LE) { - T lowerBound = SpoonUtil.minimumValue(this.ctLiteral).getValue(); + T lowerBound = ExpressionUtil.minimumValue(this.ctLiteral).getValue(); - if (SpoonUtil.resolveCtExpression(this.ctExpression) instanceof CtLiteral exprLiteral) { + if (ExpressionUtil.resolveCtExpression(this.ctExpression) instanceof CtLiteral exprLiteral) { lowerBound = exprLiteral.getValue(); } return Range.of(lowerBound, this.ctLiteral.getValue()); } else if (this.operator == BinaryOperatorKind.GE) { // >= - T upperBound = SpoonUtil.maximumValue(this.ctLiteral).getValue(); - if (SpoonUtil.resolveCtExpression(this.ctExpression) instanceof CtLiteral exprLiteral) { + T upperBound = ExpressionUtil.maximumValue(this.ctLiteral).getValue(); + if (ExpressionUtil.resolveCtExpression(this.ctExpression) instanceof CtLiteral exprLiteral) { upperBound = exprLiteral.getValue(); } diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/integrated/ElementUtil.java b/autograder-core/src/main/java/de/firemage/autograder/core/integrated/ElementUtil.java new file mode 100644 index 00000000..b1760983 --- /dev/null +++ b/autograder-core/src/main/java/de/firemage/autograder/core/integrated/ElementUtil.java @@ -0,0 +1,183 @@ +package de.firemage.autograder.core.integrated; + +import spoon.processing.FactoryAccessor; +import spoon.reflect.code.CtJavaDoc; +import spoon.reflect.cu.SourcePosition; +import spoon.reflect.declaration.CtElement; +import spoon.reflect.declaration.CtExecutable; +import spoon.reflect.declaration.CtPackage; +import spoon.reflect.declaration.CtParameter; + +import java.util.Collections; +import java.util.HashSet; +import java.util.IdentityHashMap; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; + +public final class ElementUtil { + private ElementUtil() { + } + + + public static boolean isAnyNestedOrSame(CtElement ctElement, Set potentialParents) { + // CtElement::hasParent will recursively call itself until it reaches the root + // => inefficient and might cause a stack overflow + + if (potentialParents.contains(ctElement)) { + return true; + } + + for (CtElement parent : parents(ctElement)) { + if (potentialParents.contains(parent)) { + return true; + } + } + + return false; + } + + public static CtPackage getRootPackage(FactoryAccessor element) { + return element.getFactory().getModel().getRootPackage(); + } + + public static boolean isNestedOrSame(CtElement element, CtElement parent) { + Set set = Collections.newSetFromMap(new IdentityHashMap<>()); + set.add(parent); + + return element == parent || ElementUtil.isAnyNestedOrSame(element, set); + } + + /** + * Returns an iterable over all parents of the given element. + * + * @param ctElement the element to get the parents of + * @return an iterable over all parents, the given element is not included + */ + public static Iterable parents(CtElement ctElement) { + return () -> new Iterator<>() { + private CtElement current = ctElement; + + @Override + public boolean hasNext() { + return this.current.isParentInitialized(); + } + + @Override + public CtElement next() throws NoSuchElementException { + if (!this.hasNext()) { + throw new NoSuchElementException("No more parents"); + } + + CtElement result = this.current.getParent(); + this.current = result; + return result; + } + }; + } + + public static

P getParentOrSelf(CtElement element, Class

parentType) { + Objects.requireNonNull(element); + if (parentType.isAssignableFrom(element.getClass())) { + return (P) element; + } + return element.getParent(parentType); + } + + public static int getParameterIndex(CtParameter parameter, CtExecutable executable) { + for (int i = 0; i < executable.getParameters().size(); i++) { + if (executable.getParameters().get(i) == parameter) { + return i; + } + } + throw new IllegalArgumentException("Parameter not found in executable"); + } + + public static Optional getJavadoc(CtElement element) { + if (element.getComments().isEmpty() || !(element.getComments().get(0) instanceof CtJavaDoc)) { + // TODO lookup inherited javadoc + return Optional.empty(); + } else { + return Optional.of(element.getComments().get(0).asJavaDoc()); + } + } + + private record IdentityKey(T value) { + public static IdentityKey of(T value) { + return new IdentityKey<>(value); + } + + @Override + public int hashCode() { + return System.identityHashCode(this.value); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || this.getClass() != obj.getClass()) { + return false; + } + IdentityKey that = (IdentityKey) obj; + return this.value == that.value(); + } + } + + private static HashSet newHashSet(Iterator elements, Function mapper) { + HashSet set = new HashSet<>(); + for (; elements.hasNext(); set.add(mapper.apply(elements.next()))); + return set; + } + /** + * Finds the closest common parent of the given elements. + * + * @param firstElement the first element to find the common parent of + * @param others any amount of other elements to find the common parent of + * @return the closest common parent of the given elements or the firstElement itself if others is empty + */ + public static CtElement findCommonParent(CtElement firstElement, Iterable others) { + // CtElement::hasParent will recursively call itself until it reaches the root + // => inefficient and might cause a stack overflow + + // add all parents of the firstElement to a set sorted by distance to the firstElement: + Set> ctParents = new LinkedHashSet<>(); + ctParents.add(IdentityKey.of(firstElement)); + parents(firstElement).forEach(element -> ctParents.add(IdentityKey.of(element))); + + for (CtElement other : others) { + // only keep the parents that the firstElement and the other have in common + ctParents.retainAll(newHashSet(parents(other).iterator(), IdentityKey::of)); + } + + // the first element in the set is the closest common parent + return ctParents.iterator().next().value(); + } + + public static SourcePosition findPosition(CtElement ctElement) { + if (ctElement.getPosition().isValidPosition()) { + return ctElement.getPosition(); + } + + for (CtElement element : parents(ctElement)) { + if (element.getPosition().isValidPosition()) { + return element.getPosition(); + } + } + + return null; + } + + public static CtElement findValidPosition(CtElement ctElement) { + CtElement result = ctElement; + while (result != null && !result.getPosition().isValidPosition()) { + result = result.getParent(); + } + return result; + } +} diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/integrated/ExpressionUtil.java b/autograder-core/src/main/java/de/firemage/autograder/core/integrated/ExpressionUtil.java new file mode 100644 index 00000000..ef3784ee --- /dev/null +++ b/autograder-core/src/main/java/de/firemage/autograder/core/integrated/ExpressionUtil.java @@ -0,0 +1,681 @@ +package de.firemage.autograder.core.integrated; + +import de.firemage.autograder.core.check.api.UseEnumValues; +import de.firemage.autograder.core.integrated.evaluator.Evaluator; +import de.firemage.autograder.core.integrated.evaluator.fold.FoldUtils; +import de.firemage.autograder.core.integrated.evaluator.fold.InlineVariableRead; +import de.firemage.autograder.core.integrated.evaluator.fold.RemoveRedundantCasts; +import spoon.reflect.CtModel; +import spoon.reflect.code.BinaryOperatorKind; +import spoon.reflect.code.CtBinaryOperator; +import spoon.reflect.code.CtConditional; +import spoon.reflect.code.CtExpression; +import spoon.reflect.code.CtFieldRead; +import spoon.reflect.code.CtInvocation; +import spoon.reflect.code.CtLiteral; +import spoon.reflect.code.CtTextBlock; +import spoon.reflect.code.CtTypeAccess; +import spoon.reflect.code.CtUnaryOperator; +import spoon.reflect.code.CtVariableRead; +import spoon.reflect.code.LiteralBase; +import spoon.reflect.code.UnaryOperatorKind; +import spoon.reflect.declaration.CtElement; +import spoon.reflect.declaration.CtEnum; +import spoon.reflect.declaration.CtEnumValue; +import spoon.reflect.declaration.CtTypedElement; +import spoon.reflect.declaration.CtVariable; +import spoon.reflect.eval.PartialEvaluator; +import spoon.reflect.reference.CtExecutableReference; +import spoon.reflect.reference.CtTypeReference; +import spoon.reflect.visitor.CtScanner; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.BiPredicate; +import java.util.function.Predicate; +import java.util.function.UnaryOperator; +import java.util.stream.Stream; + +public final class ExpressionUtil { + private ExpressionUtil() { + } + + public static Optional> isToStringCall(CtExpression expression) { + if (!TypeUtil.isString(expression.getType())) { + return Optional.empty(); + } + + if (expression instanceof CtInvocation invocation && + MethodUtil.isSignatureEqualTo(invocation.getExecutable(), String.class, "toString")) { + return Optional.of(invocation.getTarget().getType()); + } else { + return Optional.empty(); + } + } + + public static boolean isStringLiteral(CtExpression expression, String value) { + return expression instanceof CtLiteral literal && literal.getValue() != null && + literal.getValue().equals(value); + } + + public static boolean isNullLiteral(CtExpression expression) { + return resolveConstant(expression) instanceof CtLiteral literal && literal.getValue() == null; + } + + public static boolean isIntegerLiteral(CtExpression expression, int value) { + return expression instanceof CtLiteral literal && literal.getValue().equals(value); + } + + public static boolean isBoolean(CtTypedElement ctTypedElement) { + CtTypeReference ctTypeReference = ctTypedElement.getType(); + return ctTypeReference != null && TypeUtil.isTypeEqualTo(ctTypeReference, boolean.class, Boolean.class); + } + + public static Optional tryGetBooleanLiteral(CtExpression expression) { + if (resolveConstant(expression) instanceof CtLiteral literal + && literal.getValue() != null + && isBoolean(literal)) { + + return Optional.of((Boolean) literal.getValue()); + } else { + return Optional.empty(); + } + } + + public static Optional tryGetStringLiteral(CtExpression expression) { + if (resolveConstant(expression) instanceof CtLiteral literal + && literal.getValue() != null + && TypeUtil.isTypeEqualTo(literal.getType(), String.class)) { + + return Optional.of((String) literal.getValue()); + } else { + return Optional.empty(); + } + } + + // equals impl of CtLiteral seems to be broken + public static boolean areLiteralsEqual( + CtLiteral left, + CtLiteral right + ) { + if (left == null && right == null) { + return true; + } else if (left == null || right == null) { + return false; + } + + if (left.getValue() == null) { + return right.getValue() == null; + } else if (right.getValue() == null) { + return false; + } + + if (left.getValue() instanceof Character l && right.getValue() instanceof Character r) { + return l.equals(r); + } else if (left.getValue() instanceof Number l && right.getValue() instanceof Character r) { + return l.intValue() == (int) r; + } else if (left.getValue() instanceof Character l && right.getValue() instanceof Number r) { + return (int) l == r.intValue(); + } + + if (!(left.getValue() instanceof Number valLeft) + || !(right.getValue() instanceof Number valRight)) { + return left.getValue() == right.getValue() || left.getValue().equals(right.getValue()); + } + + if (valLeft instanceof Float || valLeft instanceof Double || valRight instanceof Float + || valRight instanceof Double) { + return valLeft.doubleValue() == valRight.doubleValue(); + } + + return valLeft.longValue() == valRight.longValue(); + } + + public static List> getElementsOfExpression(CtExpression ctExpression) { + var supportedCollections = Stream.of( + List.class, + java.util.Set.class, + java.util.Collection.class + ).map((Class e) -> ctExpression.getFactory().Type().createReference(e)); + + List> result = new ArrayList<>(); + + CtTypeReference expressionType = ctExpression.getType(); + if (supportedCollections.noneMatch(ty -> ty.equals(expressionType) || expressionType.isSubtypeOf(ty))) { + return result; + } + + if (ctExpression instanceof CtInvocation ctInvocation + && ctInvocation.getTarget() instanceof CtTypeAccess) { + CtExecutableReference ctExecutableReference = ctInvocation.getExecutable(); + if (ctExecutableReference.getSimpleName().equals("of")) { + result.addAll(ctInvocation.getArguments()); + } + } + + return result; + } + + public static CtLiteral minimumValue(CtLiteral ctLiteral) { + CtLiteral result = ctLiteral.getFactory().createLiteral(); + result.setBase(LiteralBase.DECIMAL); + result.setType(ctLiteral.getType().clone()); + + // - byte + // - short + // - int + // - long + // - float + // - double + // - boolean + // - char + + Object value = ctLiteral.getValue(); + Map, Object> minimumValueMapping = Map.ofEntries( + Map.entry(byte.class, Byte.MIN_VALUE), + Map.entry(Byte.class, Byte.MIN_VALUE), + Map.entry(short.class, Short.MIN_VALUE), + Map.entry(Short.class, Short.MIN_VALUE), + Map.entry(int.class, Integer.MIN_VALUE), + Map.entry(Integer.class, Integer.MIN_VALUE), + Map.entry(long.class, Long.MIN_VALUE), + Map.entry(Long.class, Long.MIN_VALUE), + Map.entry(float.class, Float.MIN_VALUE), + Map.entry(Float.class, Float.MIN_VALUE), + Map.entry(double.class, Double.MIN_VALUE), + Map.entry(Double.class, Double.MIN_VALUE), + Map.entry(boolean.class, false), + Map.entry(Boolean.class, false), + Map.entry(char.class, Character.MIN_VALUE), + Map.entry(Character.class, Character.MIN_VALUE) + ); + + result.setValue(minimumValueMapping.get(value.getClass())); + + return result; + } + + public static CtLiteral maximumValue(CtLiteral ctLiteral) { + CtLiteral result = ctLiteral.getFactory().createLiteral(); + result.setBase(LiteralBase.DECIMAL); + result.setType(ctLiteral.getType().clone()); + + // - byte + // - short + // - int + // - long + // - float + // - double + // - boolean + // - char + + Object value = ctLiteral.getValue(); + Map, Object> maximumValueMapping = Map.ofEntries( + Map.entry(byte.class, Byte.MAX_VALUE), + Map.entry(Byte.class, Byte.MAX_VALUE), + Map.entry(short.class, Short.MAX_VALUE), + Map.entry(Short.class, Short.MAX_VALUE), + Map.entry(int.class, Integer.MAX_VALUE), + Map.entry(Integer.class, Integer.MAX_VALUE), + Map.entry(long.class, Long.MAX_VALUE), + Map.entry(Long.class, Long.MAX_VALUE), + Map.entry(float.class, Float.MAX_VALUE), + Map.entry(Float.class, Float.MAX_VALUE), + Map.entry(double.class, Double.MAX_VALUE), + Map.entry(Double.class, Double.MAX_VALUE), + Map.entry(boolean.class, true), + Map.entry(Boolean.class, true), + Map.entry(char.class, Character.MAX_VALUE), + Map.entry(Character.class, Character.MAX_VALUE) + ); + + result.setValue(maximumValueMapping.get(value.getClass())); + + return result; + } + + /** + * Swaps the operands of a binary operator. + * + * @param ctBinaryOperator the operator to swap, can be of any kind + * @return the cloned version with the operands swapped or the given operator if it is not supported + * @param the type the operator evaluates to + */ + @SuppressWarnings({"unchecked","rawtypes"}) + public static CtBinaryOperator swapCtBinaryOperator(CtBinaryOperator ctBinaryOperator) { + CtBinaryOperator result = ctBinaryOperator.clone(); + + CtExpression left = result.getLeftHandOperand(); + CtExpression right = result.getRightHandOperand(); + + // NOTE: this only implements a few cases, for other non-commutative operators, this will break code + result.setKind(switch (ctBinaryOperator.getKind()) { + // a < b => b > a + case LT -> BinaryOperatorKind.GT; + // a <= b => b >= a + case LE -> BinaryOperatorKind.GE; + // a >= b => b <= a + case GE -> BinaryOperatorKind.LE; + // a > b => b < a + case GT -> BinaryOperatorKind.LT; + default -> ctBinaryOperator.getKind(); + }); + + // swap the left and right + result.setLeftHandOperand(right); + result.setRightHandOperand(left); + + return result; + } + + /** + * Replaces {@link spoon.reflect.code.CtVariableRead} in the provided expression if they are effectively final + * and their value is known. + * + * @param ctExpression the expression to resolve. If it is {@code null}, then {@code null} is returned + * @return the resolved expression. It will be cloned and detached from the {@link CtModel} + * @param the type of the expression + */ + public static CtExpression resolveConstant(CtExpression ctExpression) { + if (ctExpression == null) return null; + + Evaluator evaluator = new Evaluator(InlineVariableRead.create(true)); + + return evaluator.evaluate(ctExpression); + } + + /** + * Converts a binary operator like < to <= or > to >= and adjusts the operands accordingly + * to make finding patterns on them easier by not having to special-case them. Additionally, + * one can specify a predicate to swap the operands if necessary. For example, to ensure that + * a literal is always on the right-hand side. + * + * @param shouldSwap the left and right hands are passed to it, and it should return true if they should be swapped and false if nothing should be changed + * @param ctBinaryOperator the operator to normalize, can be of any kind + * @return the normalized operator or the given operator if it is not supported + * @param the type the operator evaluates to + */ + public static CtBinaryOperator normalizeBy( + BiPredicate, ? super CtExpression> shouldSwap, + CtBinaryOperator ctBinaryOperator + ) { + CtExpression left = ctBinaryOperator.getLeftHandOperand(); + CtExpression right = ctBinaryOperator.getRightHandOperand(); + + BinaryOperatorKind operator = ctBinaryOperator.getKind(); + + CtBinaryOperator result = ctBinaryOperator.clone(); + result.setKind(operator); + result.setLeftHandOperand(left.clone()); + result.setRightHandOperand(right.clone()); + + // check if the left and right have to be swapped. To do that, the operator must be inverted: + // a <= b => b >= a + // a < b => b > a + // a >= b => b <= a + // a > b => b < a + // + // ^ in this example it is expected that the b should be on the left + if (shouldSwap.test(left, right)) { + result = swapCtBinaryOperator(result); + } + + // in this step < and > are adjusted to <= and >= : + // a < b => a <= b - 1 + // a > b => a >= b + 1 + + return ExpressionUtil.normalize(result); + } + + /** + * Converts a binary operator like 'a < b' to 'a <= b - 1' or 'a > b' to 'a >= b + 1'. + * + * @param ctBinaryOperator the operator to normalize, can be of any kind + * @return the normalized operator or the given operator if it is not supported + * @param the type the operator evaluates to + */ + private static CtBinaryOperator normalize(CtBinaryOperator ctBinaryOperator) { + // the following primitive types exist: + // - byte + // - short + // - int + // - long + // - float + // - double + // - boolean + // - char + // + // of those the following are not `Number`: + // - boolean + // - char + + if (!Set.of(BinaryOperatorKind.LT, BinaryOperatorKind.GT).contains(ctBinaryOperator.getKind()) + || !ctBinaryOperator.getRightHandOperand().getType().isPrimitive()) { + return ctBinaryOperator; + } + + // the literal to add/subtract. Simply setting it to 1 is not enough, because + // 1 is of type int and the other side might for example be a double or float + CtLiteral step = ctBinaryOperator.getFactory().Core().createLiteral(); + + Predicate> isCharacter = ty -> TypeUtil.isTypeEqualTo(ty, char.class, java.lang.Character.class); + if (isCharacter.test(ctBinaryOperator.getRightHandOperand().getType())) { + // for character use an integer literal + step.setValue((char) 1); + step.setType(ctBinaryOperator.getFactory().Type().characterPrimitiveType()); + } else { + // this assumes that < and > are only used with numbers + step.setValue(FoldUtils.convert(ctBinaryOperator.getRightHandOperand().getType(), ((Number) 1).doubleValue())); + step.setType(ctBinaryOperator.getRightHandOperand().getType()); + } + + CtBinaryOperator result = ctBinaryOperator.clone(); + if (ctBinaryOperator.getKind() == BinaryOperatorKind.LT) { + // < => <= - 1 + result.setKind(BinaryOperatorKind.LE); + result.setRightHandOperand(FactoryUtil.createBinaryOperator( + ctBinaryOperator.getRightHandOperand(), + step, + BinaryOperatorKind.MINUS + )); + } else if (ctBinaryOperator.getKind() == BinaryOperatorKind.GT) { + // > => >= + 1 + result.setKind(BinaryOperatorKind.GE); + result.setRightHandOperand(FactoryUtil.createBinaryOperator( + ctBinaryOperator.getRightHandOperand(), + step, + BinaryOperatorKind.PLUS + )); + } + + // simplify the resulting operator + result.setLeftHandOperand(ExpressionUtil.resolveCtExpression(result.getLeftHandOperand())); + // if the operand was a literal, it might have been promoted + if (result.getLeftHandOperand() instanceof CtLiteral ctLiteral) { + result.setLeftHandOperand(ExpressionUtil.castLiteral( + ExpressionUtil.getExpressionType(ctBinaryOperator.getLeftHandOperand()), + ctLiteral + )); + } + + result.setRightHandOperand(ExpressionUtil.resolveCtExpression(result.getRightHandOperand())); + if (result.getRightHandOperand() instanceof CtLiteral ctLiteral) { + result.setRightHandOperand(ExpressionUtil.castLiteral( + ExpressionUtil.getExpressionType(ctBinaryOperator.getRightHandOperand()), + ctLiteral + )); + } + + return result; + } + + @SuppressWarnings("unchecked") + public static CtExpression negate(CtExpression ctExpression) { + // !(!(a)) => a + if (ctExpression instanceof CtUnaryOperator ctUnaryOperator && ctUnaryOperator.getKind() == UnaryOperatorKind.NOT) { + return (CtExpression) ctUnaryOperator.getOperand(); + } + + if (ctExpression instanceof CtBinaryOperator ctBinaryOperator) { + CtBinaryOperator result = ctBinaryOperator.clone(); + switch (ctBinaryOperator.getKind()) { + // !(a == b) -> a != b + case EQ -> { + result.setKind(BinaryOperatorKind.NE); + return result; + } + // !(a != b) -> a == b + // + // a | b | a ^ b + // 0 | 0 | 0 + // 0 | 1 | 1 + // 1 | 0 | 1 + // 1 | 1 | 0 + // => !(a ^ b) -> a == b + case NE, BITXOR -> { + result.setKind(BinaryOperatorKind.EQ); + return result; + } + // !(a && b) -> !a || !b + case AND -> { + result.setKind(BinaryOperatorKind.OR); + result.setLeftHandOperand(negate(result.getLeftHandOperand())); + result.setRightHandOperand(negate(result.getRightHandOperand())); + return result; + } + // !(a || b) -> !a && !b + case OR -> { + result.setKind(BinaryOperatorKind.AND); + result.setLeftHandOperand(negate(result.getLeftHandOperand())); + result.setRightHandOperand(negate(result.getRightHandOperand())); + return result; + } + // !(a >= b) -> a < b + case GE -> { + result.setKind(BinaryOperatorKind.LT); + return result; + } + // !(a > b) -> a <= b + case GT -> { + result.setKind(BinaryOperatorKind.LE); + return result; + } + // !(a <= b) -> a > b + case LE -> { + result.setKind(BinaryOperatorKind.GT); + return result; + } + // !(a < b) -> a >= b + case LT -> { + result.setKind(BinaryOperatorKind.GE); + return result; + } + } + } + + return FactoryUtil.createUnaryOperator(UnaryOperatorKind.NOT, ctExpression.clone()); + } + + public static CtExpression resolveCtExpression(CtExpression ctExpression) { + if (ctExpression == null) return null; + + // Spoon's partiallyEvaluate is broken, not configurable, and fixing it would be too much work. + // Therefore, we use our own implementation. + PartialEvaluator evaluator = new Evaluator(); + + return evaluator.evaluate(ctExpression); + } + + public static CtExpression castExpression(Class targetType, CtExpression ctExpression) { + return castExpression(ctExpression.getFactory().Type().createReference(targetType), ctExpression); + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + public static CtLiteral castLiteral(CtTypeReference type, CtLiteral literal) { + CtLiteral result = literal.clone(); + result.setType(type.clone()); + + // casting a primitive to a string: + if (TypeUtil.isTypeEqualTo(type, String.class) && literal.getType().isPrimitive()) { + result.setValue(literal.getValue().toString()); + return result; + } + + // It is not possible to cast an Integer to a Double directly, which is a problem. + CtTypeReference targetType = type.unbox(); + if (targetType.isPrimitive()) { + // the FoldUtils.convert method only works for Number -> Number conversions + if (TypeUtil.isSubtypeOf(targetType.box(), Number.class)) { + // for instances of Number, one can use the convert method: + if (literal.getValue() instanceof Number number) { + result.setValue(FoldUtils.convert(type, number)); + } else { + // primitive types that do not implement Number are: + // boolean, char + + // NOTE: it does not make sense to cast a boolean to any other primitive type + if (literal.getValue() instanceof Character character) { + result.setValue(FoldUtils.convert(type, (int) character)); + } + } + } + + if (TypeUtil.isTypeEqualTo(targetType, char.class)) { + if (literal.getValue() instanceof Number number) { + result.setValue((char) number.intValue()); + } else { + result.setValue((char) literal.getValue()); + } + } else if (TypeUtil.isTypeEqualTo(targetType, boolean.class)) { + result.setValue((boolean) literal.getValue()); + } + } else { + result.setValue(type.getActualClass().cast(literal.getValue())); + } + + return result; + } + + // returns the type of the expression after applying the type casts + public static CtTypeReference getExpressionType(CtExpression ctExpression) { + CtTypeReference result = ctExpression.getType(); + + List> typeCasts = ctExpression.getTypeCasts(); + if (!typeCasts.isEmpty()) { + result = typeCasts.get(0); + } + + return result; + } + + @SuppressWarnings("unchecked") + public static > E castExpression(CtTypeReference type, CtExpression ctExpression) { + // no need to cast if the type is the same + if (getExpressionType(ctExpression).equals(type)) { + return (E) ctExpression; + } + + List> typeCasts = new ArrayList<>(ctExpression.getTypeCasts()); + typeCasts.add(0, type.clone()); + ctExpression.setTypeCasts(typeCasts); + + return (E) RemoveRedundantCasts.removeRedundantCasts(ctExpression); + } + + /** + * Checks if the given expression is a constant expression, or it satisfies the given predicate. + * + * @param expr the expression to check + * @param isAllowedExpression checks if the expression is allowed to be non-constant + * @return true if the expression is a constant expression, or it satisfies the given predicate, false otherwise + * @param the type of the expression + */ + public static boolean isConstantExpressionOr(CtExpression expr, Predicate> isAllowedExpression) { + var visitor = new CtScanner() { + private boolean isConstant; + private boolean isDone; + + private static boolean isConstantVariableRead(CtExpression expression) { + if (!(expression instanceof CtVariableRead ctVariableRead)) { + return false; + } + CtVariable ctVariable = VariableUtil.getVariableDeclaration(ctVariableRead.getVariable()); + + if (ctVariable == null) { + return false; + } + + return VariableUtil.getEffectivelyFinalExpression(ctVariable).orElse(null) instanceof CtLiteral; + } + + // use the exit instead of the enter method, so it checks the deepest nodes first. + // This way, one can assume that when a expression is encountered, + // the and are already guaranteed to be constant. + @Override + protected void exit(CtElement ctElement) { + if (!(ctElement instanceof CtExpression expression) || this.isDone) { + return; + } + + // Of all the Subinterfaces of CtExpression, these are irrelevant: + // - CtAnnotation + // - CtAnnotationFieldAccess + // - CtOperatorAssignment + // - CtArrayWrite + // - CtArrayAccess + // - CtFieldWrite + // - CtFieldAccess + // - CtVariableAccess + // - CtVariableWrite + // - CtCodeSnippetExpression (should never be encountered) + // + // these might be constant expressions, depending on more context: + // - CtArrayRead + // - CtAssignment + // - CtConstructorCall + // - CtExecutableReferenceExpression + // - CtLambda + // - CtNewArray + // - CtNewClass + // - CtSuperAccess + // - CtSwitchExpression + // - CtTargetedExpression + // - CtThisAccess + // - CtTypePattern + // + // and these are the relevant ones: + // - CtLiteral + // - CtUnaryOperator + // - CtBinaryOperator + // - CtFieldRead (if the field itself is static and final) + // - CtInvocation (if the method is static and all arguments are constant expressions) + // - CtTextBlock + // - CtConditional (ternary operator) + // - CtTypeAccess + + if (CoreUtil.isInstanceOfAny( + expression, + CtBinaryOperator.class, + UnaryOperator.class, + CtTextBlock.class, + CtConditional.class, + CtTypeAccess.class, + CtLiteral.class + )) { + this.isConstant = true; + return; + } + + if (expression instanceof CtInvocation ctInvocation && ctInvocation.getExecutable().isStatic()) { + // the arguments and target of the invocation have already been visited and all of them work in a constant context + // -> the invocation would work in a constant context as well + this.isConstant = true; + return; + } + + if (ExpressionUtil.isEnumValue(expression)) { + this.isConstant = true; + return; + } + + if (isConstantVariableRead(expression) || isAllowedExpression.test(expression)) { + this.isConstant = true; + } else { + this.isConstant = false; + this.isDone = true; + } + } + }; + + expr.accept(visitor); + + return visitor.isConstant; + } + + private static boolean isEnumValue(CtExpression ctExpression) { + return UseEnumValues.CtEnumFieldRead.of(ctExpression).isPresent(); + } +} diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/integrated/FactoryUtil.java b/autograder-core/src/main/java/de/firemage/autograder/core/integrated/FactoryUtil.java new file mode 100644 index 00000000..09f6aa80 --- /dev/null +++ b/autograder-core/src/main/java/de/firemage/autograder/core/integrated/FactoryUtil.java @@ -0,0 +1,115 @@ +package de.firemage.autograder.core.integrated; + +import de.firemage.autograder.core.integrated.evaluator.fold.FoldUtils; +import spoon.reflect.code.BinaryOperatorKind; +import spoon.reflect.code.CtBinaryOperator; +import spoon.reflect.code.CtExpression; +import spoon.reflect.code.CtInvocation; +import spoon.reflect.code.CtLiteral; +import spoon.reflect.code.CtUnaryOperator; +import spoon.reflect.code.UnaryOperatorKind; +import spoon.reflect.declaration.CtMethod; +import spoon.reflect.factory.Factory; +import spoon.reflect.reference.CtTypeReference; + +import java.util.Arrays; +import java.util.List; + +public class FactoryUtil { + private FactoryUtil() { + } + + @SuppressWarnings("unchecked") + public static CtLiteral makeLiteralNumber(CtTypeReference ctTypeReference, Number number) { + Object value = FoldUtils.convert(ctTypeReference, number); + + return makeLiteral(ctTypeReference, (T) value); + } + + /** + * Makes a new literal with the given value and type. + * + * @param ctTypeReference a reference to the type of the literal + * @param value the value of the literal + * @param the type of the value + * + * @return a new literal with the given value, note that the base is not set + */ + public static CtLiteral makeLiteral(CtTypeReference ctTypeReference, T value) { + CtLiteral literal = ctTypeReference.getFactory().createLiteral(); + literal.setType(ctTypeReference.clone()); + literal.setValue(value); + return literal; + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + public static CtBinaryOperator createBinaryOperator( + CtExpression leftHandOperand, + CtExpression rightHandOperand, + BinaryOperatorKind operatorKind + ) { + Factory factory = leftHandOperand.getFactory(); + if (factory == null) { + factory = rightHandOperand.getFactory(); + } + + CtBinaryOperator ctBinaryOperator = factory.createBinaryOperator( + leftHandOperand.clone(), + rightHandOperand.clone(), + operatorKind + ); + + if (ctBinaryOperator.getType() == null) { + ctBinaryOperator.setType(FoldUtils.inferType(ctBinaryOperator)); + } + + return ctBinaryOperator; + } + + @SuppressWarnings({"unchecked","rawtypes"}) + public static CtUnaryOperator createUnaryOperator(UnaryOperatorKind operatorKind, CtExpression ctExpression) { + CtUnaryOperator ctUnaryOperator = ctExpression.getFactory().createUnaryOperator(); + ctUnaryOperator.setOperand(ctExpression.clone()); + ctUnaryOperator.setKind(operatorKind); + + if (ctUnaryOperator.getType() == null) { + ctUnaryOperator.setType(FoldUtils.inferType(ctUnaryOperator)); + } + + return ctUnaryOperator; + } + + /** + * Creates a static invocation of the given method on the given target type. + * + * @param targetType the type on which the method is defined + * @param methodName the name of the method + * @param parameters the parameters to pass to the method + * @return the invocation + * @param the result type of the invocation + */ + public static CtInvocation createStaticInvocation( + CtTypeReference targetType, + String methodName, + CtExpression... parameters + ) { + Factory factory = targetType.getFactory(); + + CtMethod methodHandle = null; + List> potentialMethods = targetType.getTypeDeclaration().getMethodsByName(methodName); + if (potentialMethods.size() == 1) { + methodHandle = (CtMethod) potentialMethods.get(0); + } else { + methodHandle = targetType.getTypeDeclaration().getMethod( + methodName, + Arrays.stream(parameters).map(ExpressionUtil::getExpressionType).toArray(CtTypeReference[]::new) + ); + } + + return factory.createInvocation( + factory.createTypeAccess(methodHandle.getDeclaringType().getReference()), + methodHandle.getReference(), + parameters + ); + } +} diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/integrated/ForLoopRange.java b/autograder-core/src/main/java/de/firemage/autograder/core/integrated/ForLoopRange.java index b936ad1a..8b23c0d9 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/integrated/ForLoopRange.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/integrated/ForLoopRange.java @@ -33,12 +33,12 @@ public static Optional fromCtFor(CtFor ctFor) { // check that this variable is a local variable && ctVariableAccess.getVariable().getDeclaration() instanceof CtLocalVariable localVariable // which is declared before the loop - && SpoonUtil.getPreviousStatement(ctFor) + && StatementUtil.getPreviousStatement(ctFor) .map(statement -> statement instanceof CtVariable ctVariable && ctVariable.getReference().equals(ctVariableAccess.getVariable())) .orElse(false) // the loop variable must not be used after the loop - && SpoonUtil.getNextStatements(ctFor).stream().noneMatch(statement -> UsesFinder.variableUses(localVariable).nestedIn(statement).hasAny()) + && StatementUtil.getNextStatements(ctFor).stream().noneMatch(statement -> UsesFinder.variableUses(localVariable).nestedIn(statement).hasAny()) ) { potentialLoopVariable = localVariable; } else if (ctFor.getForInit().size() == 1 && ctFor.getForInit().get(0) instanceof CtLocalVariable ctLocalVariable) { @@ -46,7 +46,7 @@ public static Optional fromCtFor(CtFor ctFor) { } if (potentialLoopVariable == null - || !SpoonUtil.isTypeEqualTo(potentialLoopVariable.getType(), int.class, Integer.class) + || !TypeUtil.isTypeEqualTo(potentialLoopVariable.getType(), int.class, Integer.class) || potentialLoopVariable.getDefaultExpression() == null) { return Optional.empty(); } @@ -54,16 +54,16 @@ public static Optional fromCtFor(CtFor ctFor) { CtLocalVariable ctLocalVariable = (CtLocalVariable) potentialLoopVariable; // ensure that the loop has exactly one variable initialized with a literal value - CtExpression start = SpoonUtil.resolveCtExpression(ctLocalVariable.getDefaultExpression()); + CtExpression start = ExpressionUtil.resolveCtExpression(ctLocalVariable.getDefaultExpression()); // ensure that it is initialized with some integer - if (!(SpoonUtil.isTypeEqualTo(start.getType(), int.class, Integer.class)) + if (!(TypeUtil.isTypeEqualTo(start.getType(), int.class, Integer.class)) // validate the for expression: || ctFor.getExpression() == null // must be i <= or i < || !(ctFor.getExpression() instanceof CtBinaryOperator loopExpression) || !Set.of(BinaryOperatorKind.LT, BinaryOperatorKind.LE).contains(loopExpression.getKind()) - || !(SpoonUtil.isTypeEqualTo(loopExpression.getRightHandOperand().getType(), int.class, Integer.class)) + || !(TypeUtil.isTypeEqualTo(loopExpression.getRightHandOperand().getType(), int.class, Integer.class)) // check that the left side is the loop variable || !(loopExpression.getLeftHandOperand() instanceof CtVariableAccess ctVariableAccess) || !ctVariableAccess.getVariable().equals(ctLocalVariable.getReference()) @@ -83,12 +83,12 @@ public static Optional fromCtFor(CtFor ctFor) { // check for i <= n - 1, so it is not converted to (n - 1) + 1 if (end instanceof CtBinaryOperator ctBinaryOperator && ctBinaryOperator.getKind() == BinaryOperatorKind.MINUS - && SpoonUtil.isIntegerLiteral(SpoonUtil.resolveCtExpression(ctBinaryOperator.getRightHandOperand()), 1)) { + && ExpressionUtil.isIntegerLiteral(ExpressionUtil.resolveCtExpression(ctBinaryOperator.getRightHandOperand()), 1)) { end = (CtExpression) ctBinaryOperator.getLeftHandOperand(); } else { - end = SpoonUtil.createBinaryOperator( + end = FactoryUtil.createBinaryOperator( end, - SpoonUtil.makeLiteral(end.getType(), 1), + FactoryUtil.makeLiteral(end.getType(), 1), BinaryOperatorKind.PLUS ); } @@ -104,8 +104,8 @@ public static Optional fromCtFor(CtFor ctFor) { public CtExpression length() { CtExpression length = this.end; // special case init with 0, because end - 0 = end - if (!SpoonUtil.isIntegerLiteral(this.start, 0)) { - length = SpoonUtil.createBinaryOperator( + if (!ExpressionUtil.isIntegerLiteral(this.start, 0)) { + length = FactoryUtil.createBinaryOperator( this.end, this.start, BinaryOperatorKind.MINUS diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/integrated/IntegratedAnalysis.java b/autograder-core/src/main/java/de/firemage/autograder/core/integrated/IntegratedAnalysis.java index 85f114ca..f6e5ba83 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/integrated/IntegratedAnalysis.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/integrated/IntegratedAnalysis.java @@ -29,7 +29,7 @@ import java.util.stream.Collectors; public class IntegratedAnalysis implements CodeLinter { - private static final boolean IS_IN_DEBUG_MODE = SpoonUtil.isInJunitTest(); + private static final boolean IS_IN_DEBUG_MODE = CoreUtil.isInDebugMode(); private static final String INITIAL_INTEGRITY_CHECK_NAME = "StaticAnalysis-Constructor"; private static final boolean ENSURE_NO_ORPHANS = false; private static final boolean ENSURE_NO_MODEL_CHANGES = false; @@ -235,7 +235,7 @@ private static boolean isOrphan(CtElement ctElement) { } CtElement parent = ctElement; - Iterator iterator = SpoonUtil.parents(ctElement).iterator(); + Iterator iterator = ElementUtil.parents(ctElement).iterator(); for (; iterator.hasNext(); parent = iterator.next()) ; return parent != root; diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/integrated/MethodUtil.java b/autograder-core/src/main/java/de/firemage/autograder/core/integrated/MethodUtil.java new file mode 100644 index 00000000..51fa44ee --- /dev/null +++ b/autograder-core/src/main/java/de/firemage/autograder/core/integrated/MethodUtil.java @@ -0,0 +1,134 @@ +package de.firemage.autograder.core.integrated; + +import spoon.reflect.code.CtConstructorCall; +import spoon.reflect.code.CtInvocation; +import spoon.reflect.code.CtLambda; +import spoon.reflect.code.CtStatement; +import spoon.reflect.code.CtTypeAccess; +import spoon.reflect.declaration.CtElement; +import spoon.reflect.declaration.CtMethod; +import spoon.reflect.factory.TypeFactory; +import spoon.reflect.reference.CtExecutableReference; +import spoon.reflect.reference.CtTypeReference; + +import java.util.Arrays; +import java.util.List; + +public final class MethodUtil { + private MethodUtil() { + } + + public static boolean isMainMethod(CtMethod method) { + return method.isStatic() + && method.isPublic() + && MethodUtil.isSignatureEqualTo( + method.getReference(), + void.class, + "main", + java.lang.String[].class + ); + } + + public static boolean isSignatureEqualTo( + CtExecutableReference ctExecutableReference, + Class returnType, + String methodName, + Class... parameterTypes + ) { + TypeFactory factory = ctExecutableReference.getFactory().Type(); + return MethodUtil.isSignatureEqualTo( + ctExecutableReference, + factory.createReference(returnType), + methodName, + Arrays.stream(parameterTypes).map(factory::createReference).toArray(CtTypeReference[]::new) + ); + } + + public static boolean isSignatureEqualTo( + CtExecutableReference ctExecutableReference, + CtTypeReference returnType, + String methodName, + CtTypeReference... parameterTypes + ) { + // check that they both return the same type + if (!TypeUtil.isTypeEqualTo(ctExecutableReference.getType(), returnType)) { + return false; + } + + // their names should match: + if (!ctExecutableReference.getSimpleName().equals(methodName)) { + return false; + } + + List> givenParameters = ctExecutableReference.getParameters(); + + // the number of parameters should match + if (givenParameters.size() != parameterTypes.length) { + return false; + } + + for (int i = 0; i < parameterTypes.length; i++) { + // check if the type of the parameter is equal to the expected type + if (!TypeUtil.isTypeEqualTo(givenParameters.get(i), parameterTypes[i])) { + return false; + } + } + + return true; + } + + public static boolean isInOverridingMethod(CtElement ctElement) { + CtMethod ctMethod = ctElement.getParent(CtMethod.class); + if (ctMethod == null) { + return false; + } + + return MethodHierarchy.isOverridingMethod(ctMethod); + } + + /** + * Checks if the given method is an invocation. + * @param statement which is checked + * @return true if the statement is an invocation (instance of CtInvocation, CtConstructorCall or CtLambda), + * false otherwise + */ + public static boolean isInvocation(CtStatement statement) { + return statement instanceof CtInvocation || statement instanceof CtConstructorCall || + statement instanceof CtLambda; + } + + public static boolean isInMainMethod(CtElement ctElement) { + CtMethod ctMethod = ctElement.getParent(CtMethod.class); + if (ctMethod == null) { + return false; + } + + return MethodUtil.isMainMethod(ctMethod); + } + + public static boolean isGetter(CtMethod method) { + return method.getSimpleName().startsWith("get") + && method.getParameters().isEmpty() + && !method.getType().getSimpleName().equals("void") + && (method.isAbstract() || StatementUtil.getEffectiveStatements(method.getBody()).size() == 1); + } + + public static boolean isSetter(CtMethod method) { + return method.getSimpleName().startsWith("set") + && method.getParameters().size() == 1 + && method.getType().getSimpleName().equals("void") + && (method.isAbstract() || StatementUtil.getEffectiveStatements(method.getBody()).size() == 1); + } + + public static boolean isStaticCallTo(CtInvocation invocation, String typeName, String methodName) { + return invocation.getExecutable().isStatic() + && invocation.getTarget() instanceof CtTypeAccess access + && access.getAccessedType().getQualifiedName().equals(typeName) + && invocation.getExecutable().getSimpleName().equals(methodName); + } + + public static boolean isInSetter(CtElement ctElement) { + CtMethod parent = ctElement.getParent(CtMethod.class); + return parent != null && isSetter(parent); + } +} diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/integrated/SpoonUtil.java b/autograder-core/src/main/java/de/firemage/autograder/core/integrated/SpoonUtil.java deleted file mode 100644 index f50444f6..00000000 --- a/autograder-core/src/main/java/de/firemage/autograder/core/integrated/SpoonUtil.java +++ /dev/null @@ -1,1379 +0,0 @@ -package de.firemage.autograder.core.integrated; - -import de.firemage.autograder.core.integrated.effects.AssignmentStatement; -import de.firemage.autograder.core.integrated.effects.Effect; -import de.firemage.autograder.core.integrated.effects.TerminalEffect; -import de.firemage.autograder.core.integrated.effects.TerminalStatement; -import de.firemage.autograder.core.integrated.evaluator.Evaluator; -import de.firemage.autograder.core.integrated.evaluator.fold.FoldUtils; -import de.firemage.autograder.core.integrated.evaluator.fold.InlineVariableRead; -import de.firemage.autograder.core.integrated.evaluator.fold.RemoveRedundantCasts; -import spoon.processing.FactoryAccessor; -import spoon.reflect.CtModel; -import spoon.reflect.code.BinaryOperatorKind; -import spoon.reflect.code.CtBinaryOperator; -import spoon.reflect.code.CtBlock; -import spoon.reflect.code.CtBodyHolder; -import spoon.reflect.code.CtBreak; -import spoon.reflect.code.CtCase; -import spoon.reflect.code.CtComment; -import spoon.reflect.code.CtConstructorCall; -import spoon.reflect.code.CtExpression; -import spoon.reflect.code.CtInvocation; -import spoon.reflect.code.CtJavaDoc; -import spoon.reflect.code.CtLambda; -import spoon.reflect.code.CtLiteral; -import spoon.reflect.code.CtStatement; -import spoon.reflect.code.CtStatementList; -import spoon.reflect.code.CtTypeAccess; -import spoon.reflect.code.CtUnaryOperator; -import spoon.reflect.code.CtVariableWrite; -import spoon.reflect.code.LiteralBase; -import spoon.reflect.code.UnaryOperatorKind; -import spoon.reflect.cu.SourcePosition; -import spoon.reflect.cu.position.CompoundSourcePosition; -import spoon.reflect.declaration.CtCompilationUnit; -import spoon.reflect.declaration.CtElement; -import spoon.reflect.declaration.CtExecutable; -import spoon.reflect.declaration.CtMethod; -import spoon.reflect.declaration.CtNamedElement; -import spoon.reflect.declaration.CtPackage; -import spoon.reflect.declaration.CtParameter; -import spoon.reflect.declaration.CtType; -import spoon.reflect.declaration.CtTypeMember; -import spoon.reflect.declaration.CtTypedElement; -import spoon.reflect.declaration.CtVariable; -import spoon.reflect.declaration.ModifierKind; -import spoon.reflect.eval.PartialEvaluator; -import spoon.reflect.factory.Factory; -import spoon.reflect.factory.TypeFactory; -import spoon.reflect.reference.CtExecutableReference; -import spoon.reflect.reference.CtFieldReference; -import spoon.reflect.reference.CtReference; -import spoon.reflect.reference.CtTypeParameterReference; -import spoon.reflect.reference.CtTypeReference; -import spoon.reflect.reference.CtVariableReference; - -import org.apache.commons.io.FilenameUtils; - -import java.io.File; -import java.util.ArrayDeque; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.Deque; -import java.util.HashSet; -import java.util.IdentityHashMap; -import java.util.Iterator; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.NoSuchElementException; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; -import java.util.StringJoiner; -import java.util.function.BiPredicate; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.function.Predicate; -import java.util.stream.Stream; - -public final class SpoonUtil { - private SpoonUtil() { - - } - - public static boolean isInJunitTest() { - return Arrays.stream(Thread.currentThread().getStackTrace()) - .anyMatch(element -> element.getClassName().startsWith("org.junit.")); - } - - public static boolean isString(CtTypeReference type) { - return isTypeEqualTo(type, java.lang.String.class); - } - - public static Optional> isToStringCall(CtExpression expression) { - if (!SpoonUtil.isString(expression.getType())) { - return Optional.empty(); - } - - if (expression instanceof CtInvocation invocation && - SpoonUtil.isSignatureEqualTo(invocation.getExecutable(), java.lang.String.class, "toString")) { - return Optional.of(invocation.getTarget().getType()); - } else { - return Optional.empty(); - } - } - - public static boolean isStringLiteral(CtExpression expression, String value) { - return expression instanceof CtLiteral literal && literal.getValue() != null && - literal.getValue().equals(value); - } - - public static boolean isNullLiteral(CtExpression expression) { - return SpoonUtil.resolveConstant(expression) instanceof CtLiteral literal && literal.getValue() == null; - } - - public static boolean isIntegerLiteral(CtExpression expression, int value) { - return expression instanceof CtLiteral literal && literal.getValue().equals(value); - } - - public static boolean isBoolean(CtTypedElement ctTypedElement) { - CtTypeReference ctTypeReference = ctTypedElement.getType(); - return ctTypeReference != null && SpoonUtil.isTypeEqualTo(ctTypeReference, boolean.class, Boolean.class); - } - - public static Optional tryGetBooleanLiteral(CtExpression expression) { - if (SpoonUtil.resolveConstant(expression) instanceof CtLiteral literal - && literal.getValue() != null - && isBoolean(literal)) { - - return Optional.of((Boolean) literal.getValue()); - } else { - return Optional.empty(); - } - } - - public static Optional tryGetStringLiteral(CtExpression expression) { - if (SpoonUtil.resolveConstant(expression) instanceof CtLiteral literal - && literal.getValue() != null - && isTypeEqualTo(literal.getType(), java.lang.String.class)) { - - return Optional.of((String) literal.getValue()); - } else { - return Optional.empty(); - } - } - - // equals impl of CtLiteral seems to be broken - public static boolean areLiteralsEqual( - CtLiteral left, - CtLiteral right - ) { - if (left == null && right == null) { - return true; - } else if (left == null || right == null) { - return false; - } - - if (left.getValue() == null) { - return right.getValue() == null; - } else if (right.getValue() == null) { - return false; - } - - if (left.getValue() instanceof Character l && right.getValue() instanceof Character r) { - return l.equals(r); - } else if (left.getValue() instanceof Number l && right.getValue() instanceof Character r) { - return l.intValue() == (int) r; - } else if (left.getValue() instanceof Character l && right.getValue() instanceof Number r) { - return (int) l == r.intValue(); - } - - if (!(left.getValue() instanceof Number valLeft) - || !(right.getValue() instanceof Number valRight)) { - return left.getValue() == right.getValue() || left.getValue().equals(right.getValue()); - } - - if (valLeft instanceof Float || valLeft instanceof Double || valRight instanceof Float - || valRight instanceof Double) { - return valLeft.doubleValue() == valRight.doubleValue(); - } - - return valLeft.longValue() == valRight.longValue(); - } - - @SuppressWarnings("unchecked") - public static CtLiteral makeLiteralNumber(CtTypeReference ctTypeReference, Number number) { - Object value = FoldUtils.convert(ctTypeReference, number); - - return SpoonUtil.makeLiteral(ctTypeReference, (T) value); - } - - /** - * Makes a new literal with the given value and type. - * - * @param ctTypeReference a reference to the type of the literal - * @param value the value of the literal - * @param the type of the value - * - * @return a new literal with the given value, note that the base is not set - */ - public static CtLiteral makeLiteral(CtTypeReference ctTypeReference, T value) { - CtLiteral literal = ctTypeReference.getFactory().createLiteral(); - literal.setType(ctTypeReference.clone()); - literal.setValue(value); - return literal; - } - - public static List> getElementsOfExpression(CtExpression ctExpression) { - var supportedCollections = Stream.of( - java.util.List.class, - java.util.Set.class, - java.util.Collection.class - ).map((Class e) -> ctExpression.getFactory().Type().createReference(e)); - - List> result = new ArrayList<>(); - - CtTypeReference expressionType = ctExpression.getType(); - if (supportedCollections.noneMatch(ty -> ty.equals(expressionType) || expressionType.isSubtypeOf(ty))) { - return result; - } - - if (ctExpression instanceof CtInvocation ctInvocation - && ctInvocation.getTarget() instanceof CtTypeAccess) { - CtExecutableReference ctExecutableReference = ctInvocation.getExecutable(); - if (ctExecutableReference.getSimpleName().equals("of")) { - result.addAll(ctInvocation.getArguments()); - } - } - - return result; - } - - private static List getEffectiveStatements(Collection statements) { - return statements.stream().flatMap(ctStatement -> { - // flatten blocks - if (ctStatement instanceof CtStatementList ctStatementList) { - return getEffectiveStatements(ctStatementList.getStatements()).stream(); - } else { - return Stream.of(ctStatement); - } - }).filter(statement -> !(statement instanceof CtComment)).toList(); - } - - - public static CtLiteral minimumValue(CtLiteral ctLiteral) { - CtLiteral result = ctLiteral.getFactory().createLiteral(); - result.setBase(LiteralBase.DECIMAL); - result.setType(ctLiteral.getType().clone()); - - // - byte - // - short - // - int - // - long - // - float - // - double - // - boolean - // - char - - Object value = ctLiteral.getValue(); - Map, Object> minimumValueMapping = Map.ofEntries( - Map.entry(byte.class, Byte.MIN_VALUE), - Map.entry(Byte.class, Byte.MIN_VALUE), - Map.entry(short.class, Short.MIN_VALUE), - Map.entry(Short.class, Short.MIN_VALUE), - Map.entry(int.class, Integer.MIN_VALUE), - Map.entry(Integer.class, Integer.MIN_VALUE), - Map.entry(long.class, Long.MIN_VALUE), - Map.entry(Long.class, Long.MIN_VALUE), - Map.entry(float.class, Float.MIN_VALUE), - Map.entry(Float.class, Float.MIN_VALUE), - Map.entry(double.class, Double.MIN_VALUE), - Map.entry(Double.class, Double.MIN_VALUE), - Map.entry(boolean.class, false), - Map.entry(Boolean.class, false), - Map.entry(char.class, Character.MIN_VALUE), - Map.entry(Character.class, Character.MIN_VALUE) - ); - - result.setValue(minimumValueMapping.get(value.getClass())); - - return result; - } - - public static CtLiteral maximumValue(CtLiteral ctLiteral) { - CtLiteral result = ctLiteral.getFactory().createLiteral(); - result.setBase(LiteralBase.DECIMAL); - result.setType(ctLiteral.getType().clone()); - - // - byte - // - short - // - int - // - long - // - float - // - double - // - boolean - // - char - - Object value = ctLiteral.getValue(); - Map, Object> maximumValueMapping = Map.ofEntries( - Map.entry(byte.class, Byte.MAX_VALUE), - Map.entry(Byte.class, Byte.MAX_VALUE), - Map.entry(short.class, Short.MAX_VALUE), - Map.entry(Short.class, Short.MAX_VALUE), - Map.entry(int.class, Integer.MAX_VALUE), - Map.entry(Integer.class, Integer.MAX_VALUE), - Map.entry(long.class, Long.MAX_VALUE), - Map.entry(Long.class, Long.MAX_VALUE), - Map.entry(float.class, Float.MAX_VALUE), - Map.entry(Float.class, Float.MAX_VALUE), - Map.entry(double.class, Double.MAX_VALUE), - Map.entry(Double.class, Double.MAX_VALUE), - Map.entry(boolean.class, true), - Map.entry(Boolean.class, true), - Map.entry(char.class, Character.MAX_VALUE), - Map.entry(Character.class, Character.MAX_VALUE) - ); - - result.setValue(maximumValueMapping.get(value.getClass())); - - return result; - } - - /** - * Converts a binary operator like 'a < b' to 'a <= b - 1' or 'a > b' to 'a >= b + 1'. - * - * @param ctBinaryOperator the operator to normalize, can be of any kind - * @return the normalized operator or the given operator if it is not supported - * @param the type the operator evaluates to - */ - private static CtBinaryOperator normalize(CtBinaryOperator ctBinaryOperator) { - // the following primitive types exist: - // - byte - // - short - // - int - // - long - // - float - // - double - // - boolean - // - char - // - // of those the following are not `Number`: - // - boolean - // - char - - if (!Set.of(BinaryOperatorKind.LT, BinaryOperatorKind.GT).contains(ctBinaryOperator.getKind()) - || !ctBinaryOperator.getRightHandOperand().getType().isPrimitive()) { - return ctBinaryOperator; - } - - // the literal to add/subtract. Simply setting it to 1 is not enough, because - // 1 is of type int and the other side might for example be a double or float - CtLiteral step = ctBinaryOperator.getFactory().Core().createLiteral(); - - Predicate> isCharacter = ty -> SpoonUtil.isTypeEqualTo(ty, char.class, java.lang.Character.class); - if (isCharacter.test(ctBinaryOperator.getRightHandOperand().getType())) { - // for character use an integer literal - step.setValue((char) 1); - step.setType(ctBinaryOperator.getFactory().Type().characterPrimitiveType()); - } else { - // this assumes that < and > are only used with numbers - step.setValue(FoldUtils.convert(ctBinaryOperator.getRightHandOperand().getType(), ((Number) 1).doubleValue())); - step.setType(ctBinaryOperator.getRightHandOperand().getType()); - } - - CtBinaryOperator result = ctBinaryOperator.clone(); - if (ctBinaryOperator.getKind() == BinaryOperatorKind.LT) { - // < => <= - 1 - result.setKind(BinaryOperatorKind.LE); - result.setRightHandOperand(SpoonUtil.createBinaryOperator( - ctBinaryOperator.getRightHandOperand(), - step, - BinaryOperatorKind.MINUS - )); - } else if (ctBinaryOperator.getKind() == BinaryOperatorKind.GT) { - // > => >= + 1 - result.setKind(BinaryOperatorKind.GE); - result.setRightHandOperand(SpoonUtil.createBinaryOperator( - ctBinaryOperator.getRightHandOperand(), - step, - BinaryOperatorKind.PLUS - )); - } - - // simplify the resulting operator - result.setLeftHandOperand(SpoonUtil.resolveCtExpression(result.getLeftHandOperand())); - // if the operand was a literal, it might have been promoted - if (result.getLeftHandOperand() instanceof CtLiteral ctLiteral) { - result.setLeftHandOperand(SpoonUtil.castLiteral( - SpoonUtil.getExpressionType(ctBinaryOperator.getLeftHandOperand()), - ctLiteral - )); - } - - result.setRightHandOperand(SpoonUtil.resolveCtExpression(result.getRightHandOperand())); - if (result.getRightHandOperand() instanceof CtLiteral ctLiteral) { - result.setRightHandOperand(SpoonUtil.castLiteral( - SpoonUtil.getExpressionType(ctBinaryOperator.getRightHandOperand()), - ctLiteral - )); - } - - return result; - } - - @SuppressWarnings({"unchecked", "rawtypes"}) - public static CtBinaryOperator createBinaryOperator( - CtExpression leftHandOperand, - CtExpression rightHandOperand, - BinaryOperatorKind operatorKind - ) { - Factory factory = leftHandOperand.getFactory(); - if (factory == null) { - factory = rightHandOperand.getFactory(); - } - - CtBinaryOperator ctBinaryOperator = factory.createBinaryOperator( - leftHandOperand.clone(), - rightHandOperand.clone(), - operatorKind - ); - - if (ctBinaryOperator.getType() == null) { - ctBinaryOperator.setType(FoldUtils.inferType(ctBinaryOperator)); - } - - return ctBinaryOperator; - } - - @SuppressWarnings({"unchecked","rawtypes"}) - public static CtUnaryOperator createUnaryOperator(UnaryOperatorKind operatorKind, CtExpression ctExpression) { - CtUnaryOperator ctUnaryOperator = ctExpression.getFactory().createUnaryOperator(); - ctUnaryOperator.setOperand(ctExpression.clone()); - ctUnaryOperator.setKind(operatorKind); - - if (ctUnaryOperator.getType() == null) { - ctUnaryOperator.setType(FoldUtils.inferType(ctUnaryOperator)); - } - - return ctUnaryOperator; - } - - /** - * Swaps the operands of a binary operator. - * - * @param ctBinaryOperator the operator to swap, can be of any kind - * @return the cloned version with the operands swapped or the given operator if it is not supported - * @param the type the operator evaluates to - */ - @SuppressWarnings({"unchecked","rawtypes"}) - public static CtBinaryOperator swapCtBinaryOperator(CtBinaryOperator ctBinaryOperator) { - CtBinaryOperator result = ctBinaryOperator.clone(); - - CtExpression left = result.getLeftHandOperand(); - CtExpression right = result.getRightHandOperand(); - - // NOTE: this only implements a few cases, for other non-commutative operators, this will break code - result.setKind(switch (ctBinaryOperator.getKind()) { - // a < b => b > a - case LT -> BinaryOperatorKind.GT; - // a <= b => b >= a - case LE -> BinaryOperatorKind.GE; - // a >= b => b <= a - case GE -> BinaryOperatorKind.LE; - // a > b => b < a - case GT -> BinaryOperatorKind.LT; - default -> ctBinaryOperator.getKind(); - }); - - // swap the left and right - result.setLeftHandOperand(right); - result.setRightHandOperand(left); - - return result; - } - - /** - * Replaces {@link spoon.reflect.code.CtVariableRead} in the provided expression if they are effectively final - * and their value is known. - * - * @param ctExpression the expression to resolve. If it is {@code null}, then {@code null} is returned - * @return the resolved expression. It will be cloned and detached from the {@link CtModel} - * @param the type of the expression - */ - public static CtExpression resolveConstant(CtExpression ctExpression) { - if (ctExpression == null) return null; - - Evaluator evaluator = new Evaluator(InlineVariableRead.create(true)); - - return evaluator.evaluate(ctExpression); - } - - /** - * Converts a binary operator like < to <= or > to >= and adjusts the operands accordingly - * to make finding patterns on them easier by not having to special-case them. Additionally, - * one can specify a predicate to swap the operands if necessary. For example, to ensure that - * a literal is always on the right-hand side. - * - * @param shouldSwap the left and right hands are passed to it, and it should return true if they should be swapped and false if nothing should be changed - * @param ctBinaryOperator the operator to normalize, can be of any kind - * @return the normalized operator or the given operator if it is not supported - * @param the type the operator evaluates to - */ - public static CtBinaryOperator normalizeBy( - BiPredicate, ? super CtExpression> shouldSwap, - CtBinaryOperator ctBinaryOperator - ) { - CtExpression left = ctBinaryOperator.getLeftHandOperand(); - CtExpression right = ctBinaryOperator.getRightHandOperand(); - - BinaryOperatorKind operator = ctBinaryOperator.getKind(); - - CtBinaryOperator result = ctBinaryOperator.clone(); - result.setKind(operator); - result.setLeftHandOperand(left.clone()); - result.setRightHandOperand(right.clone()); - - // check if the left and right have to be swapped. To do that, the operator must be inverted: - // a <= b => b >= a - // a < b => b > a - // a >= b => b <= a - // a > b => b < a - // - // ^ in this example it is expected that the b should be on the left - if (shouldSwap.test(left, right)) { - result = swapCtBinaryOperator(result); - } - - // in this step < and > are adjusted to <= and >= : - // a < b => a <= b - 1 - // a > b => a >= b + 1 - - return normalize(result); - } - - @SuppressWarnings("unchecked") - public static CtExpression negate(CtExpression ctExpression) { - // !(!(a)) => a - if (ctExpression instanceof CtUnaryOperator ctUnaryOperator && ctUnaryOperator.getKind() == UnaryOperatorKind.NOT) { - return (CtExpression) ctUnaryOperator.getOperand(); - } - - if (ctExpression instanceof CtBinaryOperator ctBinaryOperator) { - CtBinaryOperator result = ctBinaryOperator.clone(); - switch (ctBinaryOperator.getKind()) { - // !(a == b) -> a != b - case EQ -> { - result.setKind(BinaryOperatorKind.NE); - return result; - } - // !(a != b) -> a == b - // - // a | b | a ^ b - // 0 | 0 | 0 - // 0 | 1 | 1 - // 1 | 0 | 1 - // 1 | 1 | 0 - // => !(a ^ b) -> a == b - case NE, BITXOR -> { - result.setKind(BinaryOperatorKind.EQ); - return result; - } - // !(a && b) -> !a || !b - case AND -> { - result.setKind(BinaryOperatorKind.OR); - result.setLeftHandOperand(negate(result.getLeftHandOperand())); - result.setRightHandOperand(negate(result.getRightHandOperand())); - return result; - } - // !(a || b) -> !a && !b - case OR -> { - result.setKind(BinaryOperatorKind.AND); - result.setLeftHandOperand(negate(result.getLeftHandOperand())); - result.setRightHandOperand(negate(result.getRightHandOperand())); - return result; - } - // !(a >= b) -> a < b - case GE -> { - result.setKind(BinaryOperatorKind.LT); - return result; - } - // !(a > b) -> a <= b - case GT -> { - result.setKind(BinaryOperatorKind.LE); - return result; - } - // !(a <= b) -> a > b - case LE -> { - result.setKind(BinaryOperatorKind.GT); - return result; - } - // !(a < b) -> a >= b - case LT -> { - result.setKind(BinaryOperatorKind.GE); - return result; - } - } - } - - return createUnaryOperator(UnaryOperatorKind.NOT, ctExpression.clone()); - } - - public static List getEffectiveStatements(CtStatement ctStatement) { - if (ctStatement == null) { - return List.of(); - } - - if (ctStatement instanceof CtStatementList ctStatementList) { - return getEffectiveStatements(ctStatementList.getStatements()); - } - - return getEffectiveStatements(List.of(ctStatement)); - } - - public static List getEffectiveStatementsOf(CtBodyHolder ctBodyHolder) { - if (ctBodyHolder == null) { - return List.of(); - } - - CtStatement body = ctBodyHolder.getBody(); - if (body == null) { - return List.of(); - } - - return getEffectiveStatements(body); - } - - public static CtExpression resolveCtExpression(CtExpression ctExpression) { - if (ctExpression == null) return null; - - // Spoon's partiallyEvaluate is broken, not configurable, and fixing it would be too much work. - // Therefore, we use our own implementation. - PartialEvaluator evaluator = new Evaluator(); - - return evaluator.evaluate(ctExpression); - } - - /** - * Extracts a nested statement from a block if possible. - *

- * A statement might be in a block {@code { statement }}. - * This method will extract the statement from the block and return it. - * - * @param statement the statement to unwrap - * @return the given statement or an unwrapped version if possible - */ - public static CtStatement unwrapStatement(CtStatement statement) { - if (statement instanceof CtBlock block) { - List statements = SpoonUtil.getEffectiveStatements(block); - if (statements.size() == 1) { - return statements.get(0); - } - } - return statement; - } - - public static boolean isGetter(CtMethod method) { - return method.getSimpleName().startsWith("get") - && method.getParameters().isEmpty() - && !method.getType().getSimpleName().equals("void") - && (method.isAbstract() || getEffectiveStatements(method.getBody()).size() == 1); - } - - public static boolean isSetter(CtMethod method) { - return method.getSimpleName().startsWith("set") - && method.getParameters().size() == 1 - && method.getType().getSimpleName().equals("void") - && (method.isAbstract() || getEffectiveStatements(method.getBody()).size() == 1); - } - - public static boolean isInSetter(CtElement ctElement) { - CtMethod parent = ctElement.getParent(CtMethod.class); - return parent != null && SpoonUtil.isSetter(parent); - } - - public static boolean isPrimitiveNumeric(CtTypeReference type) { - return type.isPrimitive() - && !type.getQualifiedName().equals("boolean") - && !type.getQualifiedName().equals("char"); - } - - public static boolean isSignatureEqualTo( - CtExecutableReference ctExecutableReference, - Class returnType, - String methodName, - Class... parameterTypes - ) { - TypeFactory factory = ctExecutableReference.getFactory().Type(); - return SpoonUtil.isSignatureEqualTo( - ctExecutableReference, - factory.createReference(returnType), - methodName, - Arrays.stream(parameterTypes).map(factory::createReference).toArray(CtTypeReference[]::new) - ); - } - - public static boolean isSignatureEqualTo( - CtExecutableReference ctExecutableReference, - CtTypeReference returnType, - String methodName, - CtTypeReference... parameterTypes - ) { - // check that they both return the same type - if (!SpoonUtil.isTypeEqualTo(ctExecutableReference.getType(), returnType)) { - return false; - } - - // their names should match: - if (!ctExecutableReference.getSimpleName().equals(methodName)) { - return false; - } - - List> givenParameters = ctExecutableReference.getParameters(); - - // the number of parameters should match - if (givenParameters.size() != parameterTypes.length) { - return false; - } - - for (int i = 0; i < parameterTypes.length; i++) { - // check if the type of the parameter is equal to the expected type - if (!SpoonUtil.isTypeEqualTo(givenParameters.get(i), parameterTypes[i])) { - return false; - } - } - - return true; - } - - public static boolean hasSubtype(CtType ctType) { - return UsesFinder.subtypesOf(ctType, false).hasAny(); - } - - /** - * Creates a static invocation of the given method on the given target type. - * - * @param targetType the type on which the method is defined - * @param methodName the name of the method - * @param parameters the parameters to pass to the method - * @return the invocation - * @param the result type of the invocation - */ - public static CtInvocation createStaticInvocation( - CtTypeReference targetType, - String methodName, - CtExpression... parameters - ) { - Factory factory = targetType.getFactory(); - - CtMethod methodHandle = null; - List> potentialMethods = targetType.getTypeDeclaration().getMethodsByName(methodName); - if (potentialMethods.size() == 1) { - methodHandle = (CtMethod) potentialMethods.get(0); - } else { - methodHandle = targetType.getTypeDeclaration().getMethod( - methodName, - Arrays.stream(parameters).map(SpoonUtil::getExpressionType).toArray(CtTypeReference[]::new) - ); - } - - return factory.createInvocation( - factory.createTypeAccess(methodHandle.getDeclaringType().getReference()), - methodHandle.getReference(), - parameters - ); - } - - public static CtExpression castExpression(Class targetType, CtExpression ctExpression) { - return SpoonUtil.castExpression(ctExpression.getFactory().Type().createReference(targetType), ctExpression); - } - - @SuppressWarnings({"rawtypes", "unchecked"}) - public static CtLiteral castLiteral(CtTypeReference type, CtLiteral literal) { - CtLiteral result = literal.clone(); - result.setType(type.clone()); - - // casting a primitive to a string: - if (SpoonUtil.isTypeEqualTo(type, String.class) && literal.getType().isPrimitive()) { - result.setValue(literal.getValue().toString()); - return result; - } - - // It is not possible to cast an Integer to a Double directly, which is a problem. - CtTypeReference targetType = type.unbox(); - if (targetType.isPrimitive()) { - // the FoldUtils.convert method only works for Number -> Number conversions - if (SpoonUtil.isSubtypeOf(targetType.box(), Number.class)) { - // for instances of Number, one can use the convert method: - if (literal.getValue() instanceof Number number) { - result.setValue(FoldUtils.convert(type, number)); - } else { - // primitive types that do not implement Number are: - // boolean, char - - // NOTE: it does not make sense to cast a boolean to any other primitive type - if (literal.getValue() instanceof Character character) { - result.setValue(FoldUtils.convert(type, (int) character)); - } - } - } - - if (SpoonUtil.isTypeEqualTo(targetType, char.class)) { - if (literal.getValue() instanceof Number number) { - result.setValue((char) number.intValue()); - } else { - result.setValue((char) literal.getValue()); - } - } else if (SpoonUtil.isTypeEqualTo(targetType, boolean.class)) { - result.setValue((boolean) literal.getValue()); - } - } else { - result.setValue(type.getActualClass().cast(literal.getValue())); - } - - return result; - } - - // returns the type of the expression after applying the type casts - public static CtTypeReference getExpressionType(CtExpression ctExpression) { - CtTypeReference result = ctExpression.getType(); - - List> typeCasts = ctExpression.getTypeCasts(); - if (!typeCasts.isEmpty()) { - result = typeCasts.get(0); - } - - return result; - } - - @SuppressWarnings("unchecked") - public static > E castExpression(CtTypeReference type, CtExpression ctExpression) { - // no need to cast if the type is the same - if (SpoonUtil.getExpressionType(ctExpression).equals(type)) { - return (E) ctExpression; - } - - List> typeCasts = new ArrayList<>(ctExpression.getTypeCasts()); - typeCasts.add(0, type.clone()); - ctExpression.setTypeCasts(typeCasts); - - return (E) RemoveRedundantCasts.removeRedundantCasts(ctExpression); - } - - public static Optional getJavadoc(CtElement element) { - if (element.getComments().isEmpty() || !(element.getComments().get(0) instanceof CtJavaDoc)) { - // TODO lookup inherited javadoc - return Optional.empty(); - } else { - return Optional.of(element.getComments().get(0).asJavaDoc()); - } - } - - public static boolean isStaticCallTo(CtInvocation invocation, String typeName, String methodName) { - return invocation.getExecutable().isStatic() - && invocation.getTarget() instanceof CtTypeAccess access - && access.getAccessedType().getQualifiedName().equals(typeName) - && invocation.getExecutable().getSimpleName().equals(methodName); - } - - public static boolean isEffectivelyFinal(CtVariable ctVariable) { - if (ctVariable.getModifiers().contains(ModifierKind.FINAL)) { - return true; - } - - return UsesFinder.variableUses(ctVariable).ofType(CtVariableWrite.class).hasNone(); - } - - public static Optional> getEffectivelyFinalExpression(CtVariable ctVariable) { - if (!isEffectivelyFinal(ctVariable)) { - return Optional.empty(); - } - - return Optional.ofNullable(ctVariable.getDefaultExpression()); - } - - /** - * Checks if the given element is guaranteed to be immutable. - *

- * Note that when this method returns {@code false}, the type might still be immutable. - * - * @param ctTypeReference the type to check - * @return true if the given element is guaranteed to be immutable, false otherwise - * @param the type of the element - */ - public static boolean isImmutable(CtTypeReference ctTypeReference) { - Deque> queue = new ArrayDeque<>(Collections.singletonList(ctTypeReference)); - Collection> visited = new HashSet<>(); - - while (!queue.isEmpty()) { - CtType ctType = queue.removeFirst().getTypeDeclaration(); - - // if the type is not in the classpath, null is returned - // in those cases, assume that the type is not immutable - if (ctType == null) { - return false; - } - - // skip types that have been checked (those are guaranteed to be immutable) - if (visited.contains(ctType)) { - continue; - } - - // primitive types and strings are immutable as well: - if (ctType.getReference().unbox().isPrimitive() - || SpoonUtil.isTypeEqualTo(ctType.getReference(), java.lang.String.class)) { - continue; - } - - // types that are not in the classpath like java.util.ArrayList are shadow types. - // the source code for those is missing, so it is impossible to check if they are immutable. - // => assume they are not immutable - if (ctType.isShadow()) { - return false; - } - - // for a type to be immutable, all of its fields must be final and immutable as well: - for (CtFieldReference ctFieldReference : ctType.getAllFields()) { - if (!SpoonUtil.isEffectivelyFinal(ctFieldReference.getFieldDeclaration())) { - return false; - } - - queue.add(ctFieldReference.getType()); - } - - visited.add(ctType); - } - - return true; - } - - /** - * Checks if the given type is equal to any of the expected types. - * - * @param ctType the type to check - * @param expected all allowed types - * @return true if the given type is equal to any of the expected types, false otherwise - */ - public static boolean isTypeEqualTo(CtTypeReference ctType, Class... expected) { - TypeFactory factory = ctType.getFactory().Type(); - return SpoonUtil.isTypeEqualTo( - ctType, - Arrays.stream(expected) - .map(factory::get) - .map(CtType::getReference) - .toArray(CtTypeReference[]::new) - ); - } - - /** - * Checks if the given type is equal to any of the expected types. - * - * @param ctType the type to check - * @param expected all allowed types - * @return true if the given type is equal to any of the expected types, false otherwise - */ - public static boolean isTypeEqualTo(CtTypeReference ctType, CtTypeReference... expected) { - return Arrays.asList(expected).contains(ctType); - } - - public static boolean isSubtypeOf(CtTypeReference ctTypeReference, Class expected) { - // NOTE: calling isSubtypeOf on CtTypeParameterReference will result in a crash - CtType expectedType = ctTypeReference.getFactory().Type().get(expected); - - if (ctTypeReference.getTypeDeclaration() == null) { - return ctTypeReference.isSubtypeOf(expectedType.getReference()); - } - - boolean result = !(ctTypeReference instanceof CtTypeParameterReference) - && UsesFinder.isSubtypeOf(ctTypeReference.getTypeDeclaration(), expectedType); - - if (SpoonUtil.isInJunitTest() && result != ctTypeReference.isSubtypeOf(expectedType.getReference())) { - throw new IllegalStateException("UsesFinder.isSubtypeOf(%s, %s) does not match spoon implementation".formatted( - ctTypeReference.getQualifiedName(), - expectedType.getQualifiedName() - )); - } - - return result; - } - - public static boolean isMainMethod(CtMethod method) { - return method.isStatic() - && method.isPublic() - && SpoonUtil.isSignatureEqualTo( - method.getReference(), - void.class, - "main", - java.lang.String[].class - ); - } - - /** - * Returns an iterable over all parents of the given element. - * - * @param ctElement the element to get the parents of - * @return an iterable over all parents, the given element is not included - */ - static Iterable parents(CtElement ctElement) { - return () -> new Iterator<>() { - private CtElement current = ctElement; - - @Override - public boolean hasNext() { - return this.current.isParentInitialized(); - } - - @Override - public CtElement next() throws NoSuchElementException { - if (!this.hasNext()) { - throw new NoSuchElementException("No more parents"); - } - - CtElement result = this.current.getParent(); - this.current = result; - return result; - } - }; - } - - private static HashSet newHashSet(Iterator elements, Function mapper) { - HashSet set = new HashSet<>(); - for (; elements.hasNext(); set.add(mapper.apply(elements.next()))); - return set; - } - - private record IdentityKey(T value) { - public static IdentityKey of(T value) { - return new IdentityKey<>(value); - } - - @Override - public int hashCode() { - return System.identityHashCode(this.value); - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null || this.getClass() != obj.getClass()) { - return false; - } - IdentityKey that = (IdentityKey) obj; - return this.value == that.value(); - } - } - - public static void visitCtCompilationUnit(CtModel ctModel, Consumer lambda) { - // it is not possible to visit CtCompilationUnit through the processor API. - // - // in https://github.com/INRIA/spoon/issues/5168 the below code is mentioned as a workaround: - ctModel - .getAllTypes() - .stream() - .map(CtType::getPosition) - .filter(SourcePosition::isValidPosition) - .map(SourcePosition::getCompilationUnit) - // visit each compilation unit only once - .distinct() - .forEach(lambda); - } - - /** - * Finds the closest common parent of the given elements. - * - * @param firstElement the first element to find the common parent of - * @param others any amount of other elements to find the common parent of - * @return the closest common parent of the given elements or the firstElement itself if others is empty - */ - public static CtElement findCommonParent(CtElement firstElement, Iterable others) { - // CtElement::hasParent will recursively call itself until it reaches the root - // => inefficient and might cause a stack overflow - - // add all parents of the firstElement to a set sorted by distance to the firstElement: - Set> ctParents = new LinkedHashSet<>(); - ctParents.add(IdentityKey.of(firstElement)); - parents(firstElement).forEach(element -> ctParents.add(IdentityKey.of(element))); - - for (CtElement other : others) { - // only keep the parents that the firstElement and the other have in common - ctParents.retainAll(newHashSet(parents(other).iterator(), IdentityKey::of)); - } - - // the first element in the set is the closest common parent - return ctParents.iterator().next().value(); - } - - /** - * Checks if the given type is an inner class. - * - * @param type the type to check, not null - * @return true if the given type is an inner class, false otherwise - */ - public static boolean isInnerClass(CtTypeMember type) { - return type.getDeclaringType() != null; - } - - public static boolean isInOverridingMethod(CtElement ctElement) { - CtMethod ctMethod = ctElement.getParent(CtMethod.class); - if (ctMethod == null) { - return false; - } - - return MethodHierarchy.isOverridingMethod(ctMethod); - } - - /** - * Checks if the given method is an invocation. - * @param statement which is checked - * @return true if the statement is an invocation (instance of CtInvocation, CtConstructorCall or CtLambda), - * false otherwise - */ - public static boolean isInvocation(CtStatement statement) { - return statement instanceof CtInvocation || statement instanceof CtConstructorCall || - statement instanceof CtLambda; - } - - public static boolean isInMainMethod(CtElement ctElement) { - CtMethod ctMethod = ctElement.getParent(CtMethod.class); - if (ctMethod == null) { - return false; - } - - return isMainMethod(ctMethod); - } - - public static CtElement getReferenceDeclaration(CtReference ctReference) { - // this might be null if the reference is not in the source path - // for example, when the reference points to a java.lang type - CtElement target = ctReference.getDeclaration(); - - if (target == null && ctReference instanceof CtTypeReference ctTypeReference) { - target = ctTypeReference.getTypeDeclaration(); - } - - if (target == null && ctReference instanceof CtExecutableReference ctExecutableReference) { - target = ctExecutableReference.getExecutableDeclaration(); - } - - if (target == null && ctReference instanceof CtVariableReference ctVariableReference) { - target = getVariableDeclaration(ctVariableReference); - } - - return target; - } - - public static CtVariable getVariableDeclaration(CtVariableReference ctVariableReference) { - // this might be null if the reference is not in the source path - // for example, when the reference points to a java.lang type - CtVariable target = ctVariableReference.getDeclaration(); - - if (target == null && ctVariableReference instanceof CtFieldReference ctFieldReference) { - target = ctFieldReference.getFieldDeclaration(); - } - - return target; - } - - private static int referenceIndexOf(List list, T element) { - for (int i = 0; i < list.size(); i++) { - if (list.get(i) == element) { - return i; - } - } - - return -1; - } - - /** - * Finds the statement that is before the given statement if possible. - * - * @param ctStatement the statement to find the previous statement of, must not be null - * @return the previous statement or an empty optional if there is no previous statement - */ - public static Optional getPreviousStatement(CtStatement ctStatement) { - List previousStatements = getPreviousStatements(ctStatement); - return previousStatements.isEmpty() ? Optional.empty() : Optional.of(previousStatements.get(previousStatements.size() - 1)); - } - - public static List getPreviousStatements(CtStatement ctStatement) { - List result = new ArrayList<>(); - if (ctStatement.getParent() instanceof CtStatementList ctStatementList) { - List statements = ctStatementList.getStatements(); - int index = referenceIndexOf(statements, ctStatement); - - if (index >= 0) { - result.addAll(statements.subList(0, index)); - } - } - - return result; - } - - public static List getNextStatements(CtStatement ctStatement) { - List result = new ArrayList<>(); - if (ctStatement.getParent() instanceof CtStatementList ctStatementList) { - List statements = ctStatementList.getStatements(); - int index = referenceIndexOf(statements, ctStatement); - - if (index >= 0) { - result.addAll(statements.subList(index + 1, statements.size())); - } - } - - return result; - } - - public static String truncatedSuggestion(CtElement ctElement) { - StringJoiner result = new StringJoiner(System.lineSeparator()); - - for (String line : ctElement.toString().split("\\r?\\n")) { - int newLineLength = 0; - - // this ensures that the truncation is the same on linux and windows - if (!result.toString().contains("\r\n")) { - newLineLength += (int) result.toString().chars().filter(ch -> ch == '\n').count(); - } - - if (result.length() + newLineLength > 150) { - if (line.startsWith(" ")) { - result.add("...".indent(line.length() - line.stripIndent().length()).stripTrailing()); - } else { - result.add("..."); - } - - if (result.toString().startsWith("{")) { - result.add("}"); - } - - break; - } - - result.add(line); - } - - return result.toString(); - } - - public static Optional tryMakeEffect(CtStatement ctStatement) { - return TerminalStatement.of(ctStatement).or(() -> AssignmentStatement.of(ctStatement)); - } - - public static Optional getSingleEffect(Collection ctStatements) { - List statements = getEffectiveStatements(ctStatements); - - if (statements.size() != 1 && (statements.size() != 2 || !(statements.get(1) instanceof CtBreak))) { - return Optional.empty(); - } - - return tryMakeEffect(statements.get(0)); - } - - public static List getCasesEffects(Iterable> ctCases) { - List effects = new ArrayList<>(); - for (CtCase ctCase : ctCases) { - Optional effect = SpoonUtil.getSingleEffect(ctCase.getStatements()); - if (effect.isEmpty()) { - return new ArrayList<>(); - } - - Effect resolvedEffect = effect.get(); - - - // check for default case, which is allowed to be a terminal effect, even if the other cases are not: - if (ctCase.getCaseExpressions().isEmpty() && resolvedEffect instanceof TerminalEffect) { - continue; - } - - effects.add(resolvedEffect); - } - - if (effects.isEmpty()) return new ArrayList<>(); - - return effects; - } - - public static SourcePosition findPosition(CtElement ctElement) { - if (ctElement.getPosition().isValidPosition()) { - return ctElement.getPosition(); - } - - for (CtElement element : parents(ctElement)) { - if (element.getPosition().isValidPosition()) { - return element.getPosition(); - } - } - - return null; - } - - public static CtElement findValidPosition(CtElement ctElement) { - CtElement result = ctElement; - while (result != null && !result.getPosition().isValidPosition()) { - result = result.getParent(); - } - return result; - } - - /** - * Converts the provided source position into a human-readable string. - * - * @param sourcePosition the source position as given by spoon - * @return a human-readable string representation of the source position - */ - public static String formatSourcePosition(SourcePosition sourcePosition) { - return String.format("%s:L%d", getBaseName(sourcePosition.getFile().getName()), sourcePosition.getLine()); - } - - public static String getBaseName(String fileName) { - if (fileName == null) { - return null; - } - return FilenameUtils.removeExtension(new File(fileName).getName()); - } - - public static SourcePosition getNamePosition(CtNamedElement ctNamedElement) { - SourcePosition position = ctNamedElement.getPosition(); - - if (position instanceof CompoundSourcePosition compoundSourcePosition) { - return ctNamedElement.getFactory().createSourcePosition( - position.getCompilationUnit(), - compoundSourcePosition.getNameStart(), - compoundSourcePosition.getNameEnd(), - position.getCompilationUnit().getLineSeparatorPositions() - ); - } - - return position; - } - - public static

P getParentOrSelf(CtElement element, Class

parentType) { - Objects.requireNonNull(element); - if (parentType.isAssignableFrom(element.getClass())) { - return (P) element; - } - return element.getParent(parentType); - } - - public static int getParameterIndex(CtParameter parameter, CtExecutable executable) { - for (int i = 0; i < executable.getParameters().size(); i++) { - if (executable.getParameters().get(i) == parameter) { - return i; - } - } - throw new IllegalArgumentException("Parameter not found in executable"); - } - - public static CtPackage getRootPackage(FactoryAccessor element) { - return element.getFactory().getModel().getRootPackage(); - } - - - public static boolean isAnyNestedOrSame(CtElement ctElement, Set potentialParents) { - // CtElement::hasParent will recursively call itself until it reaches the root - // => inefficient and might cause a stack overflow - - if (potentialParents.contains(ctElement)) { - return true; - } - - for (CtElement parent : parents(ctElement)) { - if (potentialParents.contains(parent)) { - return true; - } - } - - return false; - } - - public static boolean isNestedOrSame(CtElement element, CtElement parent) { - Set set = Collections.newSetFromMap(new IdentityHashMap<>()); - set.add(parent); - - return element == parent || SpoonUtil.isAnyNestedOrSame(element, set); - } -} diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/integrated/StatementUtil.java b/autograder-core/src/main/java/de/firemage/autograder/core/integrated/StatementUtil.java new file mode 100644 index 00000000..bf34db7a --- /dev/null +++ b/autograder-core/src/main/java/de/firemage/autograder/core/integrated/StatementUtil.java @@ -0,0 +1,166 @@ +package de.firemage.autograder.core.integrated; + +import de.firemage.autograder.core.integrated.effects.AssignmentStatement; +import de.firemage.autograder.core.integrated.effects.Effect; +import de.firemage.autograder.core.integrated.effects.TerminalEffect; +import de.firemage.autograder.core.integrated.effects.TerminalStatement; +import spoon.reflect.code.CtBlock; +import spoon.reflect.code.CtBodyHolder; +import spoon.reflect.code.CtBreak; +import spoon.reflect.code.CtCase; +import spoon.reflect.code.CtComment; +import spoon.reflect.code.CtStatement; +import spoon.reflect.code.CtStatementList; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.stream.Stream; + +public class StatementUtil { + private StatementUtil() { + } + + private static List getEffectiveStatements(Collection statements) { + return statements.stream().flatMap(ctStatement -> { + // flatten blocks + if (ctStatement instanceof CtStatementList ctStatementList) { + return getEffectiveStatements(ctStatementList.getStatements()).stream(); + } else { + return Stream.of(ctStatement); + } + }).filter(statement -> !(statement instanceof CtComment)).toList(); + } + + public static List getEffectiveStatements(CtStatement ctStatement) { + if (ctStatement == null) { + return List.of(); + } + + if (ctStatement instanceof CtStatementList ctStatementList) { + return getEffectiveStatements(ctStatementList.getStatements()); + } + + return getEffectiveStatements(List.of(ctStatement)); + } + + public static List getEffectiveStatementsOf(CtBodyHolder ctBodyHolder) { + if (ctBodyHolder == null) { + return List.of(); + } + + CtStatement body = ctBodyHolder.getBody(); + if (body == null) { + return List.of(); + } + + return getEffectiveStatements(body); + } + + /** + * Extracts a nested statement from a block if possible. + *

+ * A statement might be in a block {@code { statement }}. + * This method will extract the statement from the block and return it. + * + * @param statement the statement to unwrap + * @return the given statement or an unwrapped version if possible + */ + public static CtStatement unwrapStatement(CtStatement statement) { + if (statement instanceof CtBlock block) { + List statements = getEffectiveStatements(block); + if (statements.size() == 1) { + return statements.get(0); + } + } + return statement; + } + + private static int referenceIndexOf(List list, T element) { + for (int i = 0; i < list.size(); i++) { + if (list.get(i) == element) { + return i; + } + } + + return -1; + } + + /** + * Finds the statement that is before the given statement if possible. + * + * @param ctStatement the statement to find the previous statement of, must not be null + * @return the previous statement or an empty optional if there is no previous statement + */ + public static Optional getPreviousStatement(CtStatement ctStatement) { + List previousStatements = getPreviousStatements(ctStatement); + return previousStatements.isEmpty() ? Optional.empty() : Optional.of(previousStatements.get(previousStatements.size() - 1)); + } + + public static List getPreviousStatements(CtStatement ctStatement) { + List result = new ArrayList<>(); + if (ctStatement.getParent() instanceof CtStatementList ctStatementList) { + List statements = ctStatementList.getStatements(); + int index = referenceIndexOf(statements, ctStatement); + + if (index >= 0) { + result.addAll(statements.subList(0, index)); + } + } + + return result; + } + + public static List getNextStatements(CtStatement ctStatement) { + List result = new ArrayList<>(); + if (ctStatement.getParent() instanceof CtStatementList ctStatementList) { + List statements = ctStatementList.getStatements(); + int index = referenceIndexOf(statements, ctStatement); + + if (index >= 0) { + result.addAll(statements.subList(index + 1, statements.size())); + } + } + + return result; + } + + public static Optional tryMakeEffect(CtStatement ctStatement) { + return TerminalStatement.of(ctStatement).or(() -> AssignmentStatement.of(ctStatement)); + } + + public static Optional getSingleEffect(Collection ctStatements) { + List statements = getEffectiveStatements(ctStatements); + + if (statements.size() != 1 && (statements.size() != 2 || !(statements.get(1) instanceof CtBreak))) { + return Optional.empty(); + } + + return tryMakeEffect(statements.get(0)); + } + + public static List getCasesEffects(Iterable> ctCases) { + List effects = new ArrayList<>(); + for (CtCase ctCase : ctCases) { + Optional effect = getSingleEffect(ctCase.getStatements()); + if (effect.isEmpty()) { + return new ArrayList<>(); + } + + Effect resolvedEffect = effect.get(); + + + // check for default case, which is allowed to be a terminal effect, even if the other cases are not: + if (ctCase.getCaseExpressions().isEmpty() && resolvedEffect instanceof TerminalEffect) { + continue; + } + + effects.add(resolvedEffect); + } + + if (effects.isEmpty()) return new ArrayList<>(); + + return effects; + } +} diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/integrated/StaticAnalysis.java b/autograder-core/src/main/java/de/firemage/autograder/core/integrated/StaticAnalysis.java index a79d68f0..c991eb82 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/integrated/StaticAnalysis.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/integrated/StaticAnalysis.java @@ -54,7 +54,7 @@ private static boolean isJavaUtilImport(CtImport ctImport) { public boolean hasJavaUtilImport() { AtomicBoolean hasImport = new AtomicBoolean(false); - SpoonUtil.visitCtCompilationUnit(this.getModel(), ctCompilationUnit -> { + CoreUtil.visitCtCompilationUnit(this.getModel(), ctCompilationUnit -> { if (hasImport.get()) { return; } diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/integrated/TypeUtil.java b/autograder-core/src/main/java/de/firemage/autograder/core/integrated/TypeUtil.java new file mode 100644 index 00000000..7164aa30 --- /dev/null +++ b/autograder-core/src/main/java/de/firemage/autograder/core/integrated/TypeUtil.java @@ -0,0 +1,234 @@ +package de.firemage.autograder.core.integrated; + +import spoon.reflect.declaration.CtType; +import spoon.reflect.declaration.CtTypeInformation; +import spoon.reflect.declaration.CtTypeMember; +import spoon.reflect.factory.TypeFactory; +import spoon.reflect.reference.CtFieldReference; +import spoon.reflect.reference.CtTypeParameterReference; +import spoon.reflect.reference.CtTypeReference; + +import java.util.ArrayDeque; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Deque; +import java.util.HashSet; +import java.util.Iterator; +import java.util.NoSuchElementException; + +/** + * Utility class for operations on types. + */ +public final class TypeUtil { + private TypeUtil() { + } + + public static boolean hasSubtype(CtType ctType) { + return UsesFinder.subtypesOf(ctType, false).hasAny(); + } + + /** + * Returns a stream that iterates over all super types of the given type. + * + * @param ctType the type to get the super types of + * @return a stream over all super types of the given type. + */ + public static CtElementStream> streamAllSuperTypes(CtTypeInformation ctType) { + return CtElementStream.of(allSuperTypes(ctType)); + } + + /** + * Returns an iterable that iterates over all super types of the given type. + * + * @param ctType the type to get the super types of + * @return an iterable that can produce multiple iterator over all super types of the given type. The iterable does not yield duplicates. + */ + public static Iterable> allSuperTypes(CtTypeInformation ctType) { + return () -> new Iterator<>() { + private final Collection visited = new HashSet<>(); + private final Deque queue; + + // we can't override the constructor of an anonymous class, so we have to do this: + { + // first we queue the superclass (if any) and then all superinterfaces + // + // we queue them here because otherwise hasNext() would return false, even if there are super types + this.queue = new ArrayDeque<>(); + if (ctType.getSuperclass() != null) { + this.queue.add(ctType.getSuperclass()); + } + this.queue.addAll(ctType.getSuperInterfaces()); + } + + @Override + public boolean hasNext() { + return !this.queue.isEmpty(); + } + + @Override + public CtType next() throws NoSuchElementException { + if (!this.hasNext()) { + throw new NoSuchElementException(); + } + + CtTypeReference result = this.queue.pollFirst(); + if (result == null) { // should only happen if we accidentally added null to the queue + throw new IllegalStateException("null type reference in queue"); + } + + this.visited.add(result); + + CtTypeReference superClass = result.getSuperclass(); + if (superClass != null) { + this.queue.add(superClass); + } + + // add all super interfaces that we haven't visited yet + for (CtTypeReference superInterface : result.getSuperInterfaces()) { + if (this.visited.add(superInterface)) { + this.queue.add(superInterface); + } + } + + // this should never happen, but we check it anyway in case the assumption is wrong + if (result.getTypeDeclaration() == null) { + throw new IllegalStateException("Type declaration is null: " + result.getQualifiedName()); + } + + return result.getTypeDeclaration(); + } + }; + } + + /** + * Checks if the given type is equal to any of the expected types. + * + * @param ctType the type to check + * @param expected all allowed types + * @return true if the given type is equal to any of the expected types, false otherwise + */ + public static boolean isTypeEqualTo(CtTypeReference ctType, Class... expected) { + return TypeUtil.isTypeEqualTo(ctType, Arrays.asList(expected)); + } + + public static boolean isTypeEqualTo(CtTypeReference ctType, Collection> expected) { + TypeFactory factory = ctType.getFactory().Type(); + return TypeUtil.isTypeEqualTo( + ctType, + expected.stream() + .map(factory::get) + .map(CtType::getReference) + .toArray(CtTypeReference[]::new) + ); + } + + /** + * Checks if the given type is equal to any of the expected types. + * + * @param ctType the type to check + * @param expected all allowed types + * @return true if the given type is equal to any of the expected types, false otherwise + */ + public static boolean isTypeEqualTo(CtTypeReference ctType, CtTypeReference... expected) { + return Arrays.asList(expected).contains(ctType); + } + + public static boolean isSubtypeOf(CtTypeReference ctTypeReference, Class expected) { + // NOTE: calling isSubtypeOf on CtTypeParameterReference will result in a crash + CtType expectedType = ctTypeReference.getFactory().Type().get(expected); + + if (ctTypeReference.getTypeDeclaration() == null) { + return ctTypeReference.isSubtypeOf(expectedType.getReference()); + } + + return !(ctTypeReference instanceof CtTypeParameterReference) + && UsesFinder.isSubtypeOf(ctTypeReference.getTypeDeclaration(), expectedType); + } + + /** + * Checks if the given type is an inner class. + * + * @param type the type to check, not null + * @return true if the given type is an inner class, false otherwise + */ + public static boolean isInnerClass(CtTypeMember type) { + return type.getDeclaringType() != null; + } + + public static boolean isString(CtTypeReference type) { + return isTypeEqualTo(type, String.class); + } + + public static boolean isPrimitiveNumeric(CtTypeReference type) { + return type.isPrimitive() + && !type.getQualifiedName().equals("boolean") + && !type.getQualifiedName().equals("char"); + } + + /** + * Checks if the given element is guaranteed to be immutable. + *

+ * Note that when this method returns {@code false}, the type might still be immutable. + * + * @param ctTypeReference the type to check + * @return true if the given element is guaranteed to be immutable, false otherwise + * @param the type of the element + */ + public static boolean isImmutable(CtTypeReference ctTypeReference) { + Deque> queue = new ArrayDeque<>(Collections.singletonList(ctTypeReference)); + Collection> visited = new HashSet<>(); + + while (!queue.isEmpty()) { + CtType ctType = queue.removeFirst().getTypeDeclaration(); + + // if the type is not in the classpath, null is returned + // in those cases, assume that the type is not immutable + if (ctType == null) { + return false; + } + + // skip types that have been checked (those are guaranteed to be immutable) + if (visited.contains(ctType)) { + continue; + } + + // primitive types and strings are immutable as well: + if (ctType.getReference().unbox().isPrimitive() + || isTypeEqualTo(ctType.getReference(), String.class)) { + continue; + } + + // types that are not in the classpath like java.util.ArrayList are shadow types. + // the source code for those is missing, so it is impossible to check if they are immutable. + // => assume they are not immutable + if (ctType.isShadow()) { + return false; + } + + // for a type to be immutable, all of its fields must be final and immutable as well: + for (CtFieldReference ctFieldReference : ctType.getAllFields()) { + if (!VariableUtil.isEffectivelyFinal(ctFieldReference.getFieldDeclaration())) { + return false; + } + + queue.add(ctFieldReference.getType()); + } + + visited.add(ctType); + } + + return true; + } + + /** + * Checks if the given type is a collection that has a defined order. + * + * @param ctTypeReference the type to check + * @return true if the given type is a collection that has a defined order, false if not or if it can not be determined + * @param the type of the element + */ + public static boolean isOrderedCollection(CtTypeReference ctTypeReference) { + return TypeUtil.isSubtypeOf(ctTypeReference, java.util.List.class); + } +} diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/integrated/UsesFinder.java b/autograder-core/src/main/java/de/firemage/autograder/core/integrated/UsesFinder.java index a484c94f..14f22c8e 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/integrated/UsesFinder.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/integrated/UsesFinder.java @@ -31,13 +31,10 @@ import java.util.ArrayDeque; import java.util.ArrayList; -import java.util.Collection; import java.util.Deque; import java.util.HashMap; -import java.util.HashSet; import java.util.IdentityHashMap; import java.util.LinkedHashSet; -import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; @@ -65,7 +62,7 @@ public static void buildFor(CtModel model) { } private static UsesFinder getFor(FactoryAccessor factoryAccessor) { - var uses = (UsesFinder) SpoonUtil.getRootPackage(factoryAccessor).getMetadata(METADATA_KEY); + var uses = (UsesFinder) ElementUtil.getRootPackage(factoryAccessor).getMetadata(METADATA_KEY); if (uses == null) { throw new IllegalArgumentException("No uses information available for this model"); } @@ -118,24 +115,60 @@ public static CtElementStream> typeUses(CtType type) { return CtElementStream.of(UsesFinder.getFor(type).scanner.typeUses.getOrDefault(type, List.of())).assumeElementType(); } - private static CtElementStream> supertypesOf(CtType type) { - return CtElementStream.of(UsesFinder.getFor(type).scanner.supertypes.getOrDefault(type, new LinkedHashSet<>())).assumeElementType(); - } + public static boolean isSubtypeOf(CtType potentialSubtype, CtType parentType) { + if (parentType == potentialSubtype + || potentialSubtype.isPrimitive() + || parentType.isPrimitive()) { + return true; + } + + Set knownSubtypes = UsesFinder.getFor(potentialSubtype).scanner.subtypes.getOrDefault(parentType, new LinkedHashSet<>()); + + // all types that are not shadow types, should be present + // in the source code and therefore in the set of known subtypes + if (!potentialSubtype.isShadow()) { + return knownSubtypes.contains(potentialSubtype); + } + + // for shadow types we can't rely on the set of known subtypes (they might be incomplete) + // + // but if the potential subtype is already known to be a subtype of the parent type, + // we can return true immediately + if (knownSubtypes.contains(potentialSubtype)) { + return true; + } + + boolean result = TypeUtil.streamAllSuperTypes(potentialSubtype).anyMatch(type -> parentType == type); + + // this is just a sanity check to ensure that our implementation is correct + if (CoreUtil.isInDebugMode() && result != potentialSubtype.isSubtypeOf(parentType.getReference())) { + throw new IllegalStateException("Inconsistent subtype information for %s and %s".formatted( + potentialSubtype.getQualifiedName(), + parentType.getQualifiedName() + )); + } - public static boolean isSubtypeOf(CtType parentType, CtType potentialSubtype) { - return parentType == potentialSubtype - // this only caches classes declared in the model - || UsesFinder.supertypesOf(parentType).anyMatch(type -> potentialSubtype == type) - // that is why we have to call the original method - || parentType.isSubtypeOf(potentialSubtype.getReference()); + // keep track of the result for future queries + if (result) { + knownSubtypes.add(potentialSubtype); + } + + return result; } + /** + * Returns the subtypes in the model of the given type. + * + * @param type the type should be declared in the source code and not a shadow type like java.util.List, for these types the result will be empty + * @param includeSelf whether the given type should be included in the result + * @return a stream of subtypes of the given type + */ public static CtElementStream> subtypesOf(CtType type, boolean includeSelf) { Stream> selfStream = includeSelf ? Stream.of(type) : Stream.empty(); return CtElementStream.concat( selfStream, CtElementStream.of(UsesFinder.getFor(type).scanner.subtypes.getOrDefault(type, new LinkedHashSet<>())).assumeElementType() - ); + ).filter(ctType -> !ctType.isShadow()); } // It is difficult to determine whether a variable is being accessed by the variable access, @@ -165,7 +198,6 @@ private static class UsesScanner extends CtScanner { private final Map> executableUses = new IdentityHashMap<>(); private final Map> typeUses = new IdentityHashMap<>(); private final Map> subtypes = new IdentityHashMap<>(); - private final Map> supertypes = new IdentityHashMap<>(); // Caches the current instanceof pattern variables, since Spoon doesn't track them yet // We are conservative: A pattern introduces a variable until the end of the current block @@ -319,28 +351,9 @@ private void recordTypeReference(CtTypeReference reference) { } } - @SuppressWarnings("unchecked") private void recordCtType(CtType ctType) { - CtTypeReference superType = ctType.getSuperclass(); - while (superType != null && superType.getTypeDeclaration() != null) { - this.subtypes.computeIfAbsent(superType.getTypeDeclaration(), k -> new LinkedHashSet<>()).add(ctType); - this.supertypes.computeIfAbsent(ctType, k -> new LinkedHashSet<>()).add(superType.getTypeDeclaration()); - superType = superType.getSuperclass(); - } - - Collection visited = new HashSet<>(); - Deque superInterfaces = new LinkedList<>(ctType.getSuperInterfaces()); - while (!superInterfaces.isEmpty()) { - CtTypeReference superInterface = superInterfaces.poll(); - // skip already visited interfaces - if (!visited.add(superInterface)) { - continue; - } - - if (superInterface.getTypeDeclaration() != null) { - this.subtypes.computeIfAbsent(superInterface.getTypeDeclaration(), k -> new LinkedHashSet<>()).add(ctType); - } - superInterfaces.addAll(superInterface.getSuperInterfaces()); + for (CtType superType : TypeUtil.allSuperTypes(ctType)) { + this.subtypes.computeIfAbsent(superType, key -> new LinkedHashSet<>()).add(ctType); } } } diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/integrated/VariableUtil.java b/autograder-core/src/main/java/de/firemage/autograder/core/integrated/VariableUtil.java new file mode 100644 index 00000000..69f74f9d --- /dev/null +++ b/autograder-core/src/main/java/de/firemage/autograder/core/integrated/VariableUtil.java @@ -0,0 +1,67 @@ +package de.firemage.autograder.core.integrated; + +import spoon.reflect.code.CtExpression; +import spoon.reflect.code.CtVariableWrite; +import spoon.reflect.declaration.CtElement; +import spoon.reflect.declaration.CtVariable; +import spoon.reflect.declaration.ModifierKind; +import spoon.reflect.reference.CtExecutableReference; +import spoon.reflect.reference.CtFieldReference; +import spoon.reflect.reference.CtReference; +import spoon.reflect.reference.CtTypeReference; +import spoon.reflect.reference.CtVariableReference; + +import java.util.Optional; + +public final class VariableUtil { + private VariableUtil() { + } + + public static boolean isEffectivelyFinal(CtVariable ctVariable) { + if (ctVariable.getModifiers().contains(ModifierKind.FINAL)) { + return true; + } + + return UsesFinder.variableUses(ctVariable).ofType(CtVariableWrite.class).hasNone(); + } + + public static Optional> getEffectivelyFinalExpression(CtVariable ctVariable) { + if (!isEffectivelyFinal(ctVariable)) { + return Optional.empty(); + } + + return Optional.ofNullable(ctVariable.getDefaultExpression()); + } + + public static CtElement getReferenceDeclaration(CtReference ctReference) { + // this might be null if the reference is not in the source path + // for example, when the reference points to a java.lang type + CtElement target = ctReference.getDeclaration(); + + if (target == null && ctReference instanceof CtTypeReference ctTypeReference) { + target = ctTypeReference.getTypeDeclaration(); + } + + if (target == null && ctReference instanceof CtExecutableReference ctExecutableReference) { + target = ctExecutableReference.getExecutableDeclaration(); + } + + if (target == null && ctReference instanceof CtVariableReference ctVariableReference) { + target = getVariableDeclaration(ctVariableReference); + } + + return target; + } + + public static CtVariable getVariableDeclaration(CtVariableReference ctVariableReference) { + // this might be null if the reference is not in the source path + // for example, when the reference points to a java.lang type + CtVariable target = ctVariableReference.getDeclaration(); + + if (target == null && ctVariableReference instanceof CtFieldReference ctFieldReference) { + target = ctFieldReference.getFieldDeclaration(); + } + + return target; + } +} diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/integrated/evaluator/OperatorHelper.java b/autograder-core/src/main/java/de/firemage/autograder/core/integrated/evaluator/OperatorHelper.java index 30f24d97..eee8d371 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/integrated/evaluator/OperatorHelper.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/integrated/evaluator/OperatorHelper.java @@ -1,6 +1,6 @@ package de.firemage.autograder.core.integrated.evaluator; -import de.firemage.autograder.core.integrated.SpoonUtil; +import de.firemage.autograder.core.integrated.ExpressionUtil; import spoon.reflect.code.BinaryOperatorKind; import spoon.reflect.code.CtExpression; import spoon.reflect.code.CtVariableRead; @@ -175,8 +175,8 @@ public static Optional> getPromotedType( CtExpression right ) { TypeFactory typeFactory = left.getFactory().Type(); - CtTypeReference leftType = SpoonUtil.getExpressionType(left); - CtTypeReference rightType = SpoonUtil.getExpressionType(right); + CtTypeReference leftType = ExpressionUtil.getExpressionType(left); + CtTypeReference rightType = ExpressionUtil.getExpressionType(right); switch (operator) { // logical operators @@ -322,7 +322,7 @@ public static Optional> getPromotedType( CtExpression operand ) { TypeFactory typeFactory = operand.getFactory().Type(); - CtTypeReference operandType = SpoonUtil.getExpressionType(operand); + CtTypeReference operandType = ExpressionUtil.getExpressionType(operand); // The type of the expression is the type of the variable. return switch (operator) { diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/integrated/evaluator/fold/ApplyCasts.java b/autograder-core/src/main/java/de/firemage/autograder/core/integrated/evaluator/fold/ApplyCasts.java index a8020c30..a370e3fa 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/integrated/evaluator/fold/ApplyCasts.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/integrated/evaluator/fold/ApplyCasts.java @@ -1,6 +1,6 @@ package de.firemage.autograder.core.integrated.evaluator.fold; -import de.firemage.autograder.core.integrated.SpoonUtil; +import de.firemage.autograder.core.integrated.ExpressionUtil; import spoon.reflect.code.CtExpression; import spoon.reflect.code.CtLiteral; import spoon.reflect.reference.CtTypeReference; @@ -38,7 +38,7 @@ public CtLiteral foldCtLiteral(CtLiteral ctLiteral) { ctLiteral.setTypeCasts(new ArrayList<>()); for (CtTypeReference cast : casts) { - result = SpoonUtil.castLiteral(cast, result); + result = ExpressionUtil.castLiteral(cast, result); } return result; diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/integrated/evaluator/fold/ApplyOperatorPromotion.java b/autograder-core/src/main/java/de/firemage/autograder/core/integrated/evaluator/fold/ApplyOperatorPromotion.java index cbef14bd..9b04efcd 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/integrated/evaluator/fold/ApplyOperatorPromotion.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/integrated/evaluator/fold/ApplyOperatorPromotion.java @@ -1,6 +1,6 @@ package de.firemage.autograder.core.integrated.evaluator.fold; -import de.firemage.autograder.core.integrated.SpoonUtil; +import de.firemage.autograder.core.integrated.ExpressionUtil; import de.firemage.autograder.core.integrated.evaluator.OperatorHelper; import spoon.reflect.code.BinaryOperatorKind; import spoon.reflect.code.CtBinaryOperator; @@ -69,11 +69,11 @@ public CtExpression foldCtBinaryOperator(CtBinaryOperator ctBinaryOper // only promote if the predicate allows it if (this.shouldApplyOnBinaryOperator.shouldApplyOn(ctBinaryOperator, ctBinaryOperator.getLeftHandOperand())) { - ctBinaryOperator.setLeftHandOperand(SpoonUtil.castExpression(promotedType, ctBinaryOperator.getLeftHandOperand())); + ctBinaryOperator.setLeftHandOperand(ExpressionUtil.castExpression(promotedType, ctBinaryOperator.getLeftHandOperand())); } if (this.shouldApplyOnBinaryOperator.shouldApplyOn(ctBinaryOperator, ctBinaryOperator.getRightHandOperand())) { - ctBinaryOperator.setRightHandOperand(SpoonUtil.castExpression(promotedType, ctBinaryOperator.getRightHandOperand())); + ctBinaryOperator.setRightHandOperand(ExpressionUtil.castExpression(promotedType, ctBinaryOperator.getRightHandOperand())); } return ctBinaryOperator; @@ -92,7 +92,7 @@ public CtExpression foldCtUnaryOperator(CtUnaryOperator ctUnaryOperato } if (this.shouldApplyOnUnaryOperator.shouldApplyOn(ctUnaryOperator, ctUnaryOperator.getOperand())) { - ctUnaryOperator.setOperand((CtExpression) SpoonUtil.castExpression(promotedType, ctUnaryOperator.getOperand())); + ctUnaryOperator.setOperand((CtExpression) ExpressionUtil.castExpression(promotedType, ctUnaryOperator.getOperand())); } return ctUnaryOperator; diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/integrated/evaluator/fold/EvaluateLiteralOperations.java b/autograder-core/src/main/java/de/firemage/autograder/core/integrated/evaluator/fold/EvaluateLiteralOperations.java index 7d267e37..afb3b054 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/integrated/evaluator/fold/EvaluateLiteralOperations.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/integrated/evaluator/fold/EvaluateLiteralOperations.java @@ -1,6 +1,6 @@ package de.firemage.autograder.core.integrated.evaluator.fold; -import de.firemage.autograder.core.integrated.SpoonUtil; +import de.firemage.autograder.core.integrated.ExpressionUtil; import de.firemage.autograder.core.integrated.evaluator.Evaluator; import de.firemage.autograder.core.integrated.evaluator.OperatorHelper; import spoon.SpoonException; @@ -48,7 +48,7 @@ public CtExpression foldCtBinaryOperator(CtBinaryOperator ctBinaryOper Object leftObject = leftLiteral.getValue(); Object rightObject = rightLiteral.getValue(); - CtTypeReference operatorType = (CtTypeReference) SpoonUtil.getExpressionType(ctBinaryOperator); + CtTypeReference operatorType = (CtTypeReference) ExpressionUtil.getExpressionType(ctBinaryOperator); Object value = switch (ctBinaryOperator.getKind()) { case AND -> (Boolean) leftObject && (Boolean) rightObject; @@ -185,7 +185,7 @@ public CtExpression foldCtUnaryOperator(CtUnaryOperator ctUnaryOperato return ctUnaryOperator; } - CtTypeReference operatorType = (CtTypeReference) SpoonUtil.getExpressionType(promotedOperator); + CtTypeReference operatorType = (CtTypeReference) ExpressionUtil.getExpressionType(promotedOperator); Object literalValue = literal.getValue(); Object value = switch (promotedOperator.getKind()) { case NOT -> !(Boolean) literalValue; diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/integrated/evaluator/fold/EvaluatePartialLiteralOperations.java b/autograder-core/src/main/java/de/firemage/autograder/core/integrated/evaluator/fold/EvaluatePartialLiteralOperations.java index 73016831..6697c556 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/integrated/evaluator/fold/EvaluatePartialLiteralOperations.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/integrated/evaluator/fold/EvaluatePartialLiteralOperations.java @@ -1,6 +1,6 @@ package de.firemage.autograder.core.integrated.evaluator.fold; -import de.firemage.autograder.core.integrated.SpoonUtil; +import de.firemage.autograder.core.integrated.ExpressionUtil; import de.firemage.autograder.core.integrated.evaluator.Evaluator; import spoon.reflect.code.CtBinaryOperator; import spoon.reflect.code.CtExpression; @@ -30,7 +30,7 @@ public CtExpression foldCtBinaryOperator(CtBinaryOperator ctBinaryOper CtExpression rightExpression = promotedOperator.getRightHandOperand(); if (!(promotedOperator.getLeftHandOperand() instanceof CtLiteral) && promotedOperator.getRightHandOperand() instanceof CtLiteral) { - promotedOperator = SpoonUtil.swapCtBinaryOperator(promotedOperator); + promotedOperator = ExpressionUtil.swapCtBinaryOperator(promotedOperator); } // ignore if both operands are not literals diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/integrated/evaluator/fold/InlineVariableRead.java b/autograder-core/src/main/java/de/firemage/autograder/core/integrated/evaluator/fold/InlineVariableRead.java index fc608681..6a41a483 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/integrated/evaluator/fold/InlineVariableRead.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/integrated/evaluator/fold/InlineVariableRead.java @@ -1,6 +1,7 @@ package de.firemage.autograder.core.integrated.evaluator.fold; -import de.firemage.autograder.core.integrated.SpoonUtil; +import de.firemage.autograder.core.integrated.VariableUtil; +import de.firemage.autograder.core.integrated.UsesFinder; import spoon.reflect.code.CtExpression; import spoon.reflect.code.CtLiteral; import spoon.reflect.code.CtLocalVariable; @@ -36,7 +37,7 @@ public CtExpression foldCtVariableRead(CtVariableRead ctVariableRead) return ctVariableRead; } - Optional> ctExpressionOptional = SpoonUtil.getEffectivelyFinalExpression(ctVariable); + Optional> ctExpressionOptional = VariableUtil.getEffectivelyFinalExpression(ctVariable); return ctExpressionOptional.flatMap(ctExpression -> { // only inline literals: diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/integrated/evaluator/fold/RemoveRedundantCasts.java b/autograder-core/src/main/java/de/firemage/autograder/core/integrated/evaluator/fold/RemoveRedundantCasts.java index 9a2d1062..a7435ffe 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/integrated/evaluator/fold/RemoveRedundantCasts.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/integrated/evaluator/fold/RemoveRedundantCasts.java @@ -1,6 +1,7 @@ package de.firemage.autograder.core.integrated.evaluator.fold; -import de.firemage.autograder.core.integrated.SpoonUtil; +import de.firemage.autograder.core.integrated.ExpressionUtil; +import de.firemage.autograder.core.integrated.TypeUtil; import spoon.reflect.code.CtExpression; import spoon.reflect.reference.CtTypeParameterReference; import spoon.reflect.reference.CtTypeReference; @@ -47,7 +48,7 @@ private static boolean canBeImplicitlyCastTo(CtTypeReference from, CtTypeRefe CtTypeReference unboxedFrom = from.unbox(); if (unboxedFrom.isPrimitive() && to.isPrimitive()) { - if (SpoonUtil.isTypeEqualTo(unboxedFrom, char.class)) { + if (TypeUtil.isTypeEqualTo(unboxedFrom, char.class)) { return CHARACTER_IMPLICIT_WIDENING.contains(to.getActualClass()); } @@ -122,7 +123,7 @@ public static CtExpression removeRedundantCasts(CtExpression ctExpress currentType = newType; } - CtTypeReference originalExpressionType = SpoonUtil.getExpressionType(ctExpression); + CtTypeReference originalExpressionType = ExpressionUtil.getExpressionType(ctExpression); CtTypeReference newExpressionType = ctExpression.getType(); if (!newCasts.isEmpty()) { newExpressionType = newCasts.get(newCasts.size() - 1); diff --git a/autograder-core/src/main/java/de/firemage/autograder/core/integrated/structure/StructuralEqualsVisitor.java b/autograder-core/src/main/java/de/firemage/autograder/core/integrated/structure/StructuralEqualsVisitor.java index 23497434..9dff8355 100644 --- a/autograder-core/src/main/java/de/firemage/autograder/core/integrated/structure/StructuralEqualsVisitor.java +++ b/autograder-core/src/main/java/de/firemage/autograder/core/integrated/structure/StructuralEqualsVisitor.java @@ -1,30 +1,23 @@ package de.firemage.autograder.core.integrated.structure; -import de.firemage.autograder.core.integrated.SpoonUtil; -import spoon.reflect.code.CtBinaryOperator; -import spoon.reflect.code.CtConditional; +import de.firemage.autograder.core.integrated.ExpressionUtil; +import de.firemage.autograder.core.integrated.VariableUtil; +import de.firemage.autograder.core.integrated.CoreUtil; import spoon.reflect.code.CtExpression; -import spoon.reflect.code.CtInvocation; -import spoon.reflect.code.CtLiteral; import spoon.reflect.code.CtLocalVariable; -import spoon.reflect.code.CtTextBlock; -import spoon.reflect.code.CtTypeAccess; import spoon.reflect.code.CtVariableRead; import spoon.reflect.declaration.CtElement; import spoon.reflect.declaration.CtField; import spoon.reflect.declaration.CtParameter; import spoon.reflect.declaration.CtVariable; import spoon.reflect.path.CtRole; -import spoon.reflect.visitor.CtScanner; import spoon.support.visitor.equals.EqualsVisitor; import java.util.LinkedHashSet; import java.util.Set; -import java.util.function.Predicate; -import java.util.function.UnaryOperator; public final class StructuralEqualsVisitor extends EqualsVisitor { - private static final boolean IS_IN_DEBUG_MODE = SpoonUtil.isInJunitTest(); + private static final boolean IS_IN_DEBUG_MODE = CoreUtil.isInDebugMode(); private static final Set ALLOWED_MISMATCHING_ROLES = Set.of( // allow mismatching comments @@ -62,116 +55,16 @@ public boolean checkEquals(CtElement left, CtElement right) { return super.checkEquals(left, right); } - private static boolean isConstantExpressionOr(CtExpression e, Predicate> isAllowedExpression) { - var visitor = new CtScanner() { - private boolean isConstant = false; - private boolean isDone = false; - - // use the exit instead of the enter method, so it checks the deepest nodes first. - // This way, one can assume that when a expression is encountered, - // the and are already guaranteed to be constant. - @Override - protected void exit(CtElement ctElement) { - if (!(ctElement instanceof CtExpression expression) || this.isDone) { - return; - } - - // Of all the Subinterfaces of CtExpression, these are irrelevant: - // - CtAnnotation - // - CtAnnotationFieldAccess - // - CtOperatorAssignment - // - CtArrayWrite - // - CtArrayAccess - // - CtFieldWrite - // - CtFieldAccess - // - CtVariableAccess - // - CtVariableWrite - // - CtCodeSnippetExpression (should never be encountered) - // - // these might be constant expressions, depending on more context: - // - CtArrayRead - // - CtAssignment - // - CtConstructorCall - // - CtExecutableReferenceExpression - // - CtLambda - // - CtNewArray - // - CtNewClass - // - CtSuperAccess - // - CtSwitchExpression - // - CtTargetedExpression - // - CtThisAccess - // - CtTypePattern - // - // and these are the relevant ones: - // - CtLiteral - // - CtUnaryOperator - // - CtBinaryOperator - // - CtFieldRead (if the field itself is static and final) - // - CtInvocation (if the method is static and all arguments are constant expressions) - // - CtTextBlock - // - CtConditional (ternary operator) - // - CtTypeAccess - - if (isInstanceOfAny( - expression, - CtBinaryOperator.class, - UnaryOperator.class, - CtTextBlock.class, - CtConditional.class, - CtTypeAccess.class - )) { - this.isConstant = true; - return; - } - - if (expression instanceof CtInvocation ctInvocation && ctInvocation.getExecutable().isStatic()) { - // the arguments and target of the invocation have already been visited and all of them work in a constant context - // -> the invocation would work in a constant context as well - this.isConstant = true; - return; - } - - if (SpoonUtil.resolveConstant(expression) instanceof CtLiteral || isAllowedExpression.test(expression)) { - this.isConstant = true; - } else { - this.isConstant = false; - this.isDone = true; - } + private static boolean isRefactorable(Object element) { + return element instanceof CtExpression ctExpression && ExpressionUtil.isConstantExpressionOr(ctExpression, e -> { + // in addition to constant expressions, it is also okay to access parameters (if they have not been modified in the method) + if (e instanceof CtVariableRead ctVariableRead) { + CtVariable ctVariable = VariableUtil.getVariableDeclaration(ctVariableRead.getVariable()); + return ctVariable instanceof CtParameter ctParameter && VariableUtil.isEffectivelyFinal(ctParameter); } - }; - - e.accept(visitor); - - return visitor.isConstant; - } - private static boolean isRefactorable(Object element) { - if (!(element instanceof CtElement)) { return false; - } - - if (element instanceof CtExpression ctExpression) { - return isConstantExpressionOr(ctExpression, e -> { - // in addition to constant expressions, it is also okay to access parameters (if they have not been modified in the method) - if (e instanceof CtVariableRead ctVariableRead) { - CtVariable ctVariable = SpoonUtil.getVariableDeclaration(ctVariableRead.getVariable()); - return ctVariable instanceof CtParameter ctParameter && SpoonUtil.isEffectivelyFinal(ctParameter); - } - - return false; - }); - } - - return false; - } - - private static boolean isInstanceOfAny(Object object, Class... classes) { - for (Class clazz : classes) { - if (clazz.isInstance(object)) { - return true; - } - } - return false; + }); } public static boolean shouldSkip(CtRole role, Object element) { @@ -185,7 +78,7 @@ public static boolean shouldSkip(CtRole role, Object element) { // NOTE: element might be a collection of CtElements - if (role == CtRole.NAME && isInstanceOfAny(element, CtLocalVariable.class, CtField.class, CtParameter.class)) { + if (role == CtRole.NAME && CoreUtil.isInstanceOfAny(element, CtLocalVariable.class, CtField.class, CtParameter.class)) { return true; } diff --git a/autograder-core/src/test/java/de/firemage/autograder/core/check/api/TestForLoopCanBeInvocation.java b/autograder-core/src/test/java/de/firemage/autograder/core/check/api/TestForLoopCanBeInvocation.java new file mode 100644 index 00000000..4bfb9bfe --- /dev/null +++ b/autograder-core/src/test/java/de/firemage/autograder/core/check/api/TestForLoopCanBeInvocation.java @@ -0,0 +1,138 @@ +package de.firemage.autograder.core.check.api; + +import de.firemage.autograder.api.LinterException; +import de.firemage.autograder.core.LocalizedMessage; +import de.firemage.autograder.core.Problem; +import de.firemage.autograder.core.ProblemType; +import de.firemage.autograder.core.check.AbstractCheckTest; +import de.firemage.autograder.api.JavaVersion; +import de.firemage.autograder.core.file.StringSourceInfo; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class TestForLoopCanBeInvocation extends AbstractCheckTest { + private static final List PROBLEM_TYPES = List.of(ProblemType.FOR_LOOP_CAN_BE_INVOCATION); + + private void assertEqualsReimplementation(Problem problem, String suggestion) { + assertEquals( + this.linter.translateMessage( + new LocalizedMessage( + "common-reimplementation", + Map.of( + "suggestion", suggestion + ) + )), + this.linter.translateMessage(problem.getExplanation()) + ); + } + + @Test + void testAddAllArray() throws LinterException, IOException { + ProblemIterator problems = this.checkIterator(StringSourceInfo.fromSourceString( + JavaVersion.JAVA_17, + "Main", + """ + import java.util.Collection; + import java.util.ArrayList; + + public class Main { + public static Collection toCollection(T[] array) { + Collection result = new ArrayList<>(); + + for (T element : array) { + result.add(element); + } + + return result; + } + } + """ + ), PROBLEM_TYPES); + + assertEqualsReimplementation(problems.next(), "result.addAll(Arrays.asList(array))"); + problems.assertExhausted(); + } + + @Test + void testAddAllCollection() throws LinterException, IOException { + ProblemIterator problems = this.checkIterator(StringSourceInfo.fromSourceString( + JavaVersion.JAVA_17, + "Main", + """ + import java.util.Collection; + import java.util.ArrayList; + + public class Main { + public static Collection toCollection(Iterable input) { + Collection result = new ArrayList<>(); + + for (T element : input) { + result.add(element); + } + + return result; + } + } + """ + ), PROBLEM_TYPES); + + assertEqualsReimplementation(problems.next(), "result.addAll(input)"); + problems.assertExhausted(); + } + + @Test + void testAddAllCast() throws LinterException, IOException { + ProblemIterator problems = this.checkIterator(StringSourceInfo.fromSourceString( + JavaVersion.JAVA_17, + "Main", + """ + import java.util.Collection; + import java.util.ArrayList; + + public class Main { + public static Collection toCollection(Iterable input) { + Collection result = new ArrayList<>(); + + for (T element : input) { + result.add((U) element); + } + + return result; + } + } + """ + ), PROBLEM_TYPES); + + problems.assertExhausted(); + } + + @Test + void testAddAllImplicitCast() throws LinterException, IOException { + ProblemIterator problems = this.checkIterator(StringSourceInfo.fromSourceString( + JavaVersion.JAVA_17, + "Main", + """ + import java.util.Collection; + import java.util.ArrayList; + + public class Main { + public static Collection toCollection(char[] input) { + Collection result = new ArrayList<>(); + + for (char element : input) { + result.add(element); + } + + return result; + } + } + """ + ), PROBLEM_TYPES); + + problems.assertExhausted(); + }} diff --git a/autograder-core/src/test/java/de/firemage/autograder/core/check/api/TestCollectionAddAll.java b/autograder-core/src/test/java/de/firemage/autograder/core/check/api/TestSequentialAddAll.java similarity index 61% rename from autograder-core/src/test/java/de/firemage/autograder/core/check/api/TestCollectionAddAll.java rename to autograder-core/src/test/java/de/firemage/autograder/core/check/api/TestSequentialAddAll.java index 6e0c2b74..214283a2 100644 --- a/autograder-core/src/test/java/de/firemage/autograder/core/check/api/TestCollectionAddAll.java +++ b/autograder-core/src/test/java/de/firemage/autograder/core/check/api/TestSequentialAddAll.java @@ -1,28 +1,45 @@ package de.firemage.autograder.core.check.api; +import de.firemage.autograder.api.JavaVersion; import de.firemage.autograder.api.LinterException; import de.firemage.autograder.core.LocalizedMessage; import de.firemage.autograder.core.Problem; import de.firemage.autograder.core.ProblemType; import de.firemage.autograder.core.check.AbstractCheckTest; -import de.firemage.autograder.api.JavaVersion; import de.firemage.autograder.core.file.StringSourceInfo; import org.junit.jupiter.api.Test; import java.io.IOException; +import java.util.Collection; import java.util.List; import java.util.Map; import static org.junit.jupiter.api.Assertions.assertEquals; -class TestCollectionAddAll extends AbstractCheckTest { +class TestSequentialAddAll extends AbstractCheckTest { private static final List PROBLEM_TYPES = List.of( - ProblemType.COLLECTION_ADD_ALL, - ProblemType.COMMON_REIMPLEMENTATION_ADD_ENUM_VALUES, - ProblemType.COMMON_REIMPLEMENTATION_ADD_ALL + ProblemType.SEQUENTIAL_ADD_ALL, + ProblemType.USE_ENUM_VALUES ); - private void assertEqualsReimplementation(Problem problem, String suggestion) { + private void assertEqualsReimplementation(Problem problem, String type, Iterable values, String collection) { + assertEquals( + this.linter.translateMessage( + new LocalizedMessage( + "common-reimplementation", + Map.of( + "suggestion", "private static final List<%s> SOME_GOOD_NAME = List.of(%s); /* ... */ %s.addAll(SOME_GOOD_NAME)".formatted( + type, + String.join(", ", values), + collection + ) + ) + )), + this.linter.translateMessage(problem.getExplanation()) + ); + } + + private void assertEqualsEnumValues(Problem problem, String suggestion) { assertEquals( this.linter.translateMessage( new LocalizedMessage( @@ -54,11 +71,63 @@ public void foo(List list) { """ ), PROBLEM_TYPES); - assertEqualsReimplementation(problems.next(), "list.addAll(List.of(\" \", \"a\", \"b\", \"c\"))"); + assertEqualsReimplementation(problems.next(), "String", List.of("\" \"", "\"a\"", "\"b\"", "\"c\""), "list"); problems.assertExhausted(); } + @Test + void testListAddPartiallyConstant() throws LinterException, IOException { + ProblemIterator problems = this.checkIterator(StringSourceInfo.fromSourceString( + JavaVersion.JAVA_17, + "Test", + """ + import java.util.List; + + public class Test { + public void foo(List list, String value) { + list.add(" "); + list.add("a"); + list.add("b"); + list.add(value); + list.add("c"); + } + } + """ + ), PROBLEM_TYPES); + + problems.assertExhausted(); + } + + @Test + void testListAddConstants() throws LinterException, IOException { + ProblemIterator problems = this.checkIterator(StringSourceInfo.fromSourceString( + JavaVersion.JAVA_17, + "Test", + """ + import java.util.List; + + public class Test { + private static final String A = "a"; + private static final String B = "b"; + private static final String C = "c"; + private static final String D = "d"; + + public void foo(List list) { + list.add(A); + list.add(B); + list.add(C); + list.add(D); + list.add(D); + } + } + """ + ), PROBLEM_TYPES); + + assertEqualsReimplementation(problems.next(), "String", List.of("A", "B", "C", "D", "D"), "list"); + + problems.assertExhausted(); + } @Test void testEnumValuesAddAllUnorderedSet() throws LinterException, IOException { @@ -87,7 +156,7 @@ public static void main(String[] args) { """ ), PROBLEM_TYPES); - assertEqualsReimplementation(problems.next(), "fruits.addAll(Arrays.asList(Fruit.values()))"); + assertEqualsEnumValues(problems.next(), "fruits.addAll(Arrays.asList(Fruit.values()))"); problems.assertExhausted(); } @@ -170,115 +239,10 @@ private static List getAvailableCardsDuplicateEnd() { """ ), PROBLEM_TYPES); - assertEqualsReimplementation(problems.next(), "availableCards.addAll(Arrays.asList(GodCard.values()))"); - assertEqualsReimplementation(problems.next(), "availableCards.addAll(List.of(GodCard.HERMES, GodCard.DEMETER, GodCard.ATLAS, GodCard.APOLLO, GodCard.ATHENA, GodCard.ARTEMIS))"); - assertEqualsReimplementation(problems.next(), "availableCards.addAll(List.of(GodCard.APOLLO, GodCard.ARTEMIS, GodCard.ATHENA, GodCard.ATLAS, GodCard.ATLAS, GodCard.DEMETER, GodCard.HERMES))"); - assertEqualsReimplementation(problems.next(), "availableCards.addAll(List.of(GodCard.APOLLO, GodCard.ARTEMIS, GodCard.ATHENA, GodCard.ATLAS, GodCard.DEMETER, GodCard.HERMES, GodCard.HERMES))"); - problems.assertExhausted(); - } - - @Test - void testAddAllArray() throws LinterException, IOException { - ProblemIterator problems = this.checkIterator(StringSourceInfo.fromSourceString( - JavaVersion.JAVA_17, - "Main", - """ - import java.util.Collection; - import java.util.ArrayList; - - public class Main { - public static Collection toCollection(T[] array) { - Collection result = new ArrayList<>(); - - for (T element : array) { - result.add(element); - } - - return result; - } - } - """ - ), PROBLEM_TYPES); - - assertEqualsReimplementation(problems.next(), "result.addAll(Arrays.asList(array))"); + assertEqualsEnumValues(problems.next(), "availableCards.addAll(Arrays.asList(GodCard.values()))"); + assertEqualsReimplementation(problems.next(), "GodCard", List.of("GodCard.HERMES", "GodCard.DEMETER", "GodCard.ATLAS", "GodCard.APOLLO", "GodCard.ATHENA", "GodCard.ARTEMIS"), "availableCards"); + assertEqualsReimplementation(problems.next(), "GodCard", List.of("GodCard.APOLLO", "GodCard.ARTEMIS", "GodCard.ATHENA", "GodCard.ATLAS", "GodCard.ATLAS", "GodCard.DEMETER", "GodCard.HERMES"), "availableCards"); + assertEqualsReimplementation(problems.next(), "GodCard", List.of("GodCard.APOLLO", "GodCard.ARTEMIS", "GodCard.ATHENA", "GodCard.ATLAS", "GodCard.DEMETER", "GodCard.HERMES", "GodCard.HERMES"), "availableCards"); problems.assertExhausted(); } - - @Test - void testAddAllCollection() throws LinterException, IOException { - ProblemIterator problems = this.checkIterator(StringSourceInfo.fromSourceString( - JavaVersion.JAVA_17, - "Main", - """ - import java.util.Collection; - import java.util.ArrayList; - - public class Main { - public static Collection toCollection(Iterable input) { - Collection result = new ArrayList<>(); - - for (T element : input) { - result.add(element); - } - - return result; - } - } - """ - ), PROBLEM_TYPES); - - assertEqualsReimplementation(problems.next(), "result.addAll(input)"); - problems.assertExhausted(); - } - - @Test - void testAddAllCast() throws LinterException, IOException { - ProblemIterator problems = this.checkIterator(StringSourceInfo.fromSourceString( - JavaVersion.JAVA_17, - "Main", - """ - import java.util.Collection; - import java.util.ArrayList; - - public class Main { - public static Collection toCollection(Iterable input) { - Collection result = new ArrayList<>(); - - for (T element : input) { - result.add((U) element); - } - - return result; - } - } - """ - ), PROBLEM_TYPES); - - problems.assertExhausted(); - } - - @Test - void testAddAllImplicitCast() throws LinterException, IOException { - ProblemIterator problems = this.checkIterator(StringSourceInfo.fromSourceString( - JavaVersion.JAVA_17, - "Main", - """ - import java.util.Collection; - import java.util.ArrayList; - - public class Main { - public static Collection toCollection(char[] input) { - Collection result = new ArrayList<>(); - - for (char element : input) { - result.add(element); - } - - return result; - } - } - """ - ), PROBLEM_TYPES); - - problems.assertExhausted(); - }} +} diff --git a/autograder-core/src/test/java/de/firemage/autograder/core/check/api/TestUseEnumValues.java b/autograder-core/src/test/java/de/firemage/autograder/core/check/api/TestUseEnumValues.java index 933e16fb..11685253 100644 --- a/autograder-core/src/test/java/de/firemage/autograder/core/check/api/TestUseEnumValues.java +++ b/autograder-core/src/test/java/de/firemage/autograder/core/check/api/TestUseEnumValues.java @@ -16,7 +16,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; class TestUseEnumValues extends AbstractCheckTest { - private static final List PROBLEM_TYPES = List.of(ProblemType.COMMON_REIMPLEMENTATION_ADD_ENUM_VALUES); + private static final List PROBLEM_TYPES = List.of(ProblemType.USE_ENUM_VALUES); private void assertEqualsReimplementation(Problem problem, String suggestion) { assertEquals( diff --git a/autograder-core/src/test/java/de/firemage/autograder/core/check/naming/TestPackageNamingConvention.java b/autograder-core/src/test/java/de/firemage/autograder/core/check/naming/TestPackageNamingConvention.java index 39511c38..f9c88314 100644 --- a/autograder-core/src/test/java/de/firemage/autograder/core/check/naming/TestPackageNamingConvention.java +++ b/autograder-core/src/test/java/de/firemage/autograder/core/check/naming/TestPackageNamingConvention.java @@ -2,6 +2,7 @@ import de.firemage.autograder.api.LinterException; import de.firemage.autograder.core.LocalizedMessage; +import de.firemage.autograder.core.Problem; import de.firemage.autograder.core.ProblemType; import de.firemage.autograder.core.file.StringSourceInfo; import de.firemage.autograder.core.check.AbstractCheckTest; @@ -14,25 +15,34 @@ import static org.junit.jupiter.api.Assertions.*; - class TestPackageNamingConvention extends AbstractCheckTest { - private static final String LOCALIZED_MESSAGE_KEY = "package-naming-convention"; + private static final List PROBLEM_TYPES = List.of( + ProblemType.PACKAGE_NAMING_CONVENTION + ); + + private void assertEqualsWrongNaming(Problem problem, String positions) { + assertEquals(ProblemType.PACKAGE_NAMING_CONVENTION, problem.getProblemType()); + assertEquals( + this.linter.translateMessage(new LocalizedMessage("package-naming-convention", Map.of("positions", positions))), + this.linter.translateMessage(problem.getExplanation()) + ); + } @Test void testDefaultPackage() throws IOException, LinterException { - var problems = super.check(StringSourceInfo.fromSourceString( + ProblemIterator problems = this.checkIterator(StringSourceInfo.fromSourceString( JavaVersion.JAVA_17, "Test", "public class Test {}" - ), List.of(ProblemType.PACKAGE_NAMING_CONVENTION)); + ), PROBLEM_TYPES); - assertEquals(0, problems.size()); + problems.assertExhausted(); } @Test void testSingleViolation() throws IOException, LinterException { - var problems = super.check(StringSourceInfo.fromSourceString( + ProblemIterator problems = this.checkIterator(StringSourceInfo.fromSourceString( JavaVersion.JAVA_17, "com.Example.Test", """ @@ -40,20 +50,15 @@ void testSingleViolation() throws IOException, LinterException { public class Test {} """ - ), List.of(ProblemType.PACKAGE_NAMING_CONVENTION)); - + ), PROBLEM_TYPES); - assertEquals(1, problems.size()); - assertEquals(ProblemType.PACKAGE_NAMING_CONVENTION, problems.get(0).getProblemType()); - assertEquals( - this.linter.translateMessage(new LocalizedMessage(LOCALIZED_MESSAGE_KEY, Map.of("positions", "Test:L1"))), - this.linter.translateMessage(problems.get(0).getExplanation()) - ); + assertEqualsWrongNaming(problems.next(), "Test:L1"); + problems.assertExhausted(); } @Test void testMultipleViolations() throws IOException, LinterException { - var problems = super.check(StringSourceInfo.fromSourceStrings( + ProblemIterator problems = this.checkIterator(StringSourceInfo.fromSourceStrings( JavaVersion.JAVA_17, Map.ofEntries( dummySourceEntry("com.Example", "Test"), @@ -62,30 +67,22 @@ void testMultipleViolations() throws IOException, LinterException { dummySourceEntry("com.Other.Pack", "OtherPack2"), dummySourceEntry("com.Other.Pack", "OtherPack3") ) - ), List.of(ProblemType.PACKAGE_NAMING_CONVENTION)); - + ), PROBLEM_TYPES); - assertEquals(1, problems.size()); - assertEquals(ProblemType.PACKAGE_NAMING_CONVENTION, problems.get(0).getProblemType()); - assertEquals( - this.linter.translateMessage(new LocalizedMessage( - LOCALIZED_MESSAGE_KEY, - Map.of("positions", "Test:L1, OtherPack:L1") - )), - this.linter.translateMessage(problems.get(0).getExplanation()) - ); + assertEqualsWrongNaming(problems.next(), "Test:L1, OtherPack:L1"); + problems.assertExhausted(); } @Test void testFalsePositive01() throws IOException, LinterException { - var problems = super.check(StringSourceInfo.fromSourceStrings( + ProblemIterator problems = this.checkIterator(StringSourceInfo.fromSourceStrings( JavaVersion.JAVA_17, Map.ofEntries( dummySourceEntry("edu.kit", "Test"), dummySourceEntry("edu.kit.informatik", "Main") ) - ), List.of(ProblemType.PACKAGE_NAMING_CONVENTION)); + ), PROBLEM_TYPES); - assertEquals(0, problems.size()); + problems.assertExhausted(); } } diff --git a/autograder-core/src/test/java/de/firemage/autograder/core/check/structure/TestDefaultPackageCheck.java b/autograder-core/src/test/java/de/firemage/autograder/core/check/structure/TestDefaultPackageCheck.java index 54896da3..1c46441f 100644 --- a/autograder-core/src/test/java/de/firemage/autograder/core/check/structure/TestDefaultPackageCheck.java +++ b/autograder-core/src/test/java/de/firemage/autograder/core/check/structure/TestDefaultPackageCheck.java @@ -2,6 +2,7 @@ import de.firemage.autograder.api.LinterException; import de.firemage.autograder.core.LocalizedMessage; +import de.firemage.autograder.core.Problem; import de.firemage.autograder.core.ProblemType; import de.firemage.autograder.core.file.StringSourceInfo; import de.firemage.autograder.core.check.AbstractCheckTest; @@ -15,11 +16,22 @@ import static org.junit.jupiter.api.Assertions.*; class TestDefaultPackageCheck extends AbstractCheckTest { - private static final String LOCALIZED_MESSAGE_KEY = "default-package"; + private static final List PROBLEM_TYPES = List.of( + ProblemType.DEFAULT_PACKAGE_USED + ); + + private void assertEqualsDefaultPackageUsed(Problem problem, String positions) { + assertEquals(ProblemType.DEFAULT_PACKAGE_USED, problem.getProblemType()); + + assertEquals( + this.linter.translateMessage(new LocalizedMessage("default-package", Map.of("positions", positions))), + this.linter.translateMessage(problem.getExplanation()) + ); + } @Test void test() throws IOException, LinterException { - var problems = super.check(StringSourceInfo.fromSourceString( + ProblemIterator problems = this.checkIterator(StringSourceInfo.fromSourceString( JavaVersion.JAVA_17, "Test", """ @@ -29,43 +41,39 @@ public static void main(String[] args) { } } """ - ), List.of(ProblemType.DEFAULT_PACKAGE_USED)); + ), PROBLEM_TYPES); - assertEquals(1, problems.size()); - assertEquals(ProblemType.DEFAULT_PACKAGE_USED, problems.get(0).getProblemType()); + assertEqualsDefaultPackageUsed(problems.next(), "Test:L1"); + problems.assertExhausted(); } @Test void testMultipleClasses() throws IOException, LinterException { - var problems = super.check(StringSourceInfo.fromSourceStrings( + ProblemIterator problems = this.checkIterator(StringSourceInfo.fromSourceStrings( JavaVersion.JAVA_17, Map.ofEntries( dummySourceEntry("com.example", "Test"), dummySourceEntry("", "Hello"), dummySourceEntry("", "World") ) - ), List.of(ProblemType.DEFAULT_PACKAGE_USED)); + ), PROBLEM_TYPES); - assertEquals(1, problems.size()); - assertEquals(ProblemType.DEFAULT_PACKAGE_USED, problems.get(0).getProblemType()); - assertEquals( - this.linter.translateMessage(new LocalizedMessage(LOCALIZED_MESSAGE_KEY, Map.of("positions", "Hello:L1, World:L1"))), - this.linter.translateMessage(problems.get(0).getExplanation()) - ); + assertEqualsDefaultPackageUsed(problems.next(), "Hello:L1, World:L1"); + problems.assertExhausted(); } @Test void testNoDefaultPackageUsed() throws LinterException, IOException { - var problems = super.check(StringSourceInfo.fromSourceStrings( + ProblemIterator problems = this.checkIterator(StringSourceInfo.fromSourceStrings( JavaVersion.JAVA_17, Map.ofEntries( dummySourceEntry("com.example", "Test"), dummySourceEntry("com.example.a", "Hello"), dummySourceEntry("com.example.b", "World") ) - ), List.of(ProblemType.DEFAULT_PACKAGE_USED)); + ), PROBLEM_TYPES); - assertEquals(0, problems.size()); + problems.assertExhausted(); } } diff --git a/autograder-core/src/test/java/de/firemage/autograder/core/check/structure/TestTooFewPackagesCheck.java b/autograder-core/src/test/java/de/firemage/autograder/core/check/structure/TestTooFewPackagesCheck.java index d2f232b6..ba480b3b 100644 --- a/autograder-core/src/test/java/de/firemage/autograder/core/check/structure/TestTooFewPackagesCheck.java +++ b/autograder-core/src/test/java/de/firemage/autograder/core/check/structure/TestTooFewPackagesCheck.java @@ -92,4 +92,3 @@ void testWithAllowedNumberOfClasses() throws IOException, LinterException { } } - diff --git a/autograder-core/src/test/java/de/firemage/autograder/core/check/unnecessary/TestUnusedCodeElementCheck.java b/autograder-core/src/test/java/de/firemage/autograder/core/check/unnecessary/TestUnusedCodeElementCheck.java index a0796866..13568e48 100644 --- a/autograder-core/src/test/java/de/firemage/autograder/core/check/unnecessary/TestUnusedCodeElementCheck.java +++ b/autograder-core/src/test/java/de/firemage/autograder/core/check/unnecessary/TestUnusedCodeElementCheck.java @@ -11,39 +11,31 @@ import org.junit.jupiter.api.Test; import java.io.IOException; -import java.util.Collection; import java.util.List; import java.util.Map; import static org.junit.jupiter.api.Assertions.assertEquals; class TestUnusedCodeElementCheck extends AbstractCheckTest { - private static final String LOCALIZED_MESSAGE_KEY = "unused-element"; - private static final ProblemType PROBLEM_TYPE = ProblemType.UNUSED_CODE_ELEMENT; - private static final List PROBLEM_TYPES = List.of(PROBLEM_TYPE, ProblemType.UNUSED_CODE_ELEMENT_PRIVATE); + private static final List PROBLEM_TYPES = List.of( + ProblemType.UNUSED_CODE_ELEMENT, + ProblemType.UNUSED_CODE_ELEMENT_PRIVATE + ); - private void assertEqualsUnused(String name, Problem problem) { + private void assertEqualsUnused(Problem problem, String name) { assertEquals( this.linter.translateMessage( new LocalizedMessage( - LOCALIZED_MESSAGE_KEY, + "unused-element", Map.of("name", name) )), this.linter.translateMessage(problem.getExplanation()) ); } - private static void assertProblemSize(int expectedSize, Collection problems) { - assertEquals( - expectedSize, - problems.size(), - "Expected %d problems, got %d: '%s'".formatted(expectedSize, problems.size(), problems) - ); - } - @Test void testUnusedField() throws LinterException, IOException { - var problems = this.check(StringSourceInfo.fromSourceStrings( + ProblemIterator problems = this.checkIterator(StringSourceInfo.fromSourceStrings( JavaVersion.JAVA_17, Map.ofEntries( Map.entry( @@ -69,14 +61,14 @@ public class Example { ) ), PROBLEM_TYPES); - assertProblemSize(2, problems); - assertEqualsUnused("exampleVariable", problems.get(0)); - assertEqualsUnused("b", problems.get(1)); + assertEqualsUnused(problems.next(), "exampleVariable"); + assertEqualsUnused(problems.next(), "b"); + problems.assertExhausted(); } @Test void testUnusedFieldWithShadowing() throws LinterException, IOException { - var problems = this.check(StringSourceInfo.fromSourceStrings( + ProblemIterator problems = this.checkIterator(StringSourceInfo.fromSourceStrings( JavaVersion.JAVA_17, Map.ofEntries( Map.entry( @@ -98,17 +90,17 @@ public static void main(String[] args) {} ) ), PROBLEM_TYPES); - assertProblemSize(5, problems); - assertEqualsUnused("a", problems.get(0)); - assertEqualsUnused("b", problems.get(1)); - assertEqualsUnused("doSomething", problems.get(2)); - assertEqualsUnused("a", problems.get(3)); - assertEqualsUnused("b", problems.get(4)); + assertEqualsUnused(problems.next(), "a"); + assertEqualsUnused(problems.next(), "b"); + assertEqualsUnused(problems.next(), "doSomething"); + assertEqualsUnused(problems.next(), "a"); + assertEqualsUnused(problems.next(), "b"); + problems.assertExhausted(); } @Test void testUnusedRecursiveMethod() throws LinterException, IOException { - var problems = this.check(StringSourceInfo.fromSourceStrings( + ProblemIterator problems = this.checkIterator(StringSourceInfo.fromSourceStrings( JavaVersion.JAVA_17, Map.ofEntries( Map.entry( @@ -126,14 +118,14 @@ public static void main(String[] args) {} ) ), PROBLEM_TYPES); - assertProblemSize(1, problems); - assertEqualsUnused("foo", problems.get(0)); + assertEqualsUnused(problems.next(), "foo"); + problems.assertExhausted(); } @Test // See: https://github.com/Feuermagier/autograder/issues/228 void testFieldUsedByInvocation() throws LinterException, IOException { - var problems = this.check(StringSourceInfo.fromSourceStrings( + ProblemIterator problems = this.checkIterator(StringSourceInfo.fromSourceStrings( JavaVersion.JAVA_17, Map.ofEntries( Map.entry( @@ -165,12 +157,12 @@ public String getRoot() { ) ), PROBLEM_TYPES); - assertEquals(0, problems.size()); + problems.assertExhausted(); } @Test void testUnusedTypeParameter() throws LinterException, IOException { - var problems = this.check(StringSourceInfo.fromSourceStrings( + ProblemIterator problems = this.checkIterator(StringSourceInfo.fromSourceStrings( JavaVersion.JAVA_17, Map.ofEntries( Map.entry( @@ -198,13 +190,13 @@ public class Graph { ) ), PROBLEM_TYPES); - assertProblemSize(1, problems); - assertEqualsUnused("T", problems.get(0)); + assertEqualsUnused(problems.next(), "T"); + problems.assertExhausted(); } @Test void testUnusedNestedTypeParameter() throws LinterException, IOException { - var problems = this.check(StringSourceInfo.fromSourceStrings( + ProblemIterator problems = this.checkIterator(StringSourceInfo.fromSourceStrings( JavaVersion.JAVA_17, Map.ofEntries( Map.entry( @@ -228,13 +220,13 @@ class Dog{} ) ), PROBLEM_TYPES); - assertProblemSize(1, problems); - assertEqualsUnused("X", problems.get(0)); + assertEqualsUnused(problems.next(), "X"); + problems.assertExhausted(); } @Test void testUsedWildcardBound() throws LinterException, IOException { - var problems = this.check(StringSourceInfo.fromSourceStrings( + ProblemIterator problems = this.checkIterator(StringSourceInfo.fromSourceStrings( JavaVersion.JAVA_17, Map.ofEntries( Map.entry( @@ -260,13 +252,13 @@ class Dog { ) ), PROBLEM_TYPES); - assertProblemSize(0, problems); + problems.assertExhausted(); } @Test void testOnlyWrittenVariable() throws LinterException, IOException { // For now, this is not detected as unused, because it might result in false positives - var problems = this.check(StringSourceInfo.fromSourceStrings( + ProblemIterator problems = this.checkIterator(StringSourceInfo.fromSourceStrings( JavaVersion.JAVA_17, Map.ofEntries( Map.entry( @@ -284,14 +276,14 @@ public static void main(String[] args) { ) ), PROBLEM_TYPES); - assertProblemSize(1, problems); - assertEqualsUnused("arg1", problems.get(0)); - // assertEqualsUnused("arg2", problems.get(1)); + assertEqualsUnused(problems.next(), "arg1"); + // assertEqualsUnused("arg2", problems.next()); + problems.assertExhausted(); } @Test void testUnusedMainMethod() throws LinterException, IOException { - var problems = this.check(StringSourceInfo.fromSourceStrings( + ProblemIterator problems = this.checkIterator(StringSourceInfo.fromSourceStrings( JavaVersion.JAVA_17, Map.ofEntries( Map.entry( @@ -306,12 +298,12 @@ public class Main { ) ), PROBLEM_TYPES); - assertProblemSize(0, problems); + problems.assertExhausted(); } @Test void testUnusedMainMethodDefaultPackage() throws LinterException, IOException { - var problems = this.check(StringSourceInfo.fromSourceStrings( + ProblemIterator problems = this.checkIterator(StringSourceInfo.fromSourceStrings( JavaVersion.JAVA_17, Map.ofEntries( Map.entry( @@ -326,12 +318,12 @@ public static void main(String[] args) {} ) ), PROBLEM_TYPES); - assertProblemSize(0, problems); + problems.assertExhausted(); } @Test void testUnusedExternalOverriddenMethod() throws LinterException, IOException { - var problems = this.check(StringSourceInfo.fromSourceStrings( + ProblemIterator problems = this.checkIterator(StringSourceInfo.fromSourceStrings( JavaVersion.JAVA_17, Map.ofEntries( Map.entry( @@ -350,12 +342,12 @@ public static void main(String[] args) {} ) ), PROBLEM_TYPES); - assertProblemSize(0, problems); + problems.assertExhausted(); } @Test void testIndirectlyUsedEnumVariant() throws LinterException, IOException { - var problems = this.check(StringSourceInfo.fromSourceStrings( + ProblemIterator problems = this.checkIterator(StringSourceInfo.fromSourceStrings( JavaVersion.JAVA_17, Map.ofEntries( Map.entry( @@ -381,13 +373,13 @@ public enum MyEnum { ) ), PROBLEM_TYPES); - assertProblemSize(0, problems); + problems.assertExhausted(); } @Test @Disabled("Unused types are not detected for now, because of potential false-positives") void testUnusedType() throws LinterException, IOException { - var problems = this.check(StringSourceInfo.fromSourceStrings( + ProblemIterator problems = this.checkIterator(StringSourceInfo.fromSourceStrings( JavaVersion.JAVA_17, Map.ofEntries( Map.entry( @@ -411,9 +403,9 @@ public enum MyEnum { // not ok ) ), PROBLEM_TYPES); - assertProblemSize(2, problems); - assertEqualsUnused("InnerClass", problems.get(0)); - assertEqualsUnused("MyEnum", problems.get(1)); + assertEqualsUnused(problems.next(), "InnerClass"); + assertEqualsUnused(problems.next(), "MyEnum"); + problems.assertExhausted(); } @Test @@ -434,14 +426,14 @@ public static void main(String[] args) {} ) ), PROBLEM_TYPES); - assertEqualsUnused("Main()", problems.next()); + assertEqualsUnused(problems.next(), "Main()"); problems.assertExhausted(); } @Test void testUnusedPrivateConstructor() throws LinterException, IOException { - var problems = this.check(StringSourceInfo.fromSourceStrings( + ProblemIterator problems = this.checkIterator(StringSourceInfo.fromSourceStrings( JavaVersion.JAVA_17, Map.ofEntries( Map.entry( @@ -457,7 +449,7 @@ public static void main(String[] args) {} ) ), PROBLEM_TYPES); - assertProblemSize(0, problems); + problems.assertExhausted(); } @Test @@ -490,7 +482,7 @@ private void helper() {} //# not ok ) ), PROBLEM_TYPES); - assertEqualsUnused("helper", problems.next()); + assertEqualsUnused(problems.next(), "helper"); problems.assertExhausted(); } @@ -725,7 +717,7 @@ void b(String parameterName) {} ) ), PROBLEM_TYPES); - assertEqualsUnused("parameterName", problems.next()); + assertEqualsUnused(problems.next(), "parameterName"); problems.assertExhausted(); } diff --git a/autograder-core/src/test/java/de/firemage/autograder/core/integrated/evaluator/TestEvaluator.java b/autograder-core/src/test/java/de/firemage/autograder/core/integrated/evaluator/TestEvaluator.java index 79dece00..b7fe3c19 100644 --- a/autograder-core/src/test/java/de/firemage/autograder/core/integrated/evaluator/TestEvaluator.java +++ b/autograder-core/src/test/java/de/firemage/autograder/core/integrated/evaluator/TestEvaluator.java @@ -1,6 +1,6 @@ package de.firemage.autograder.core.integrated.evaluator; -import de.firemage.autograder.core.integrated.SpoonUtil; +import de.firemage.autograder.core.integrated.ExpressionUtil; import de.firemage.autograder.core.integrated.UsesFinder; import de.firemage.autograder.core.integrated.evaluator.fold.ApplyCasts; import de.firemage.autograder.core.integrated.evaluator.fold.ChainedFold; @@ -85,11 +85,11 @@ private static void runExpressionTest(String expression, String arguments, Strin CtExpression ctExpression = createExpression(expression, arguments); // the type should not change throughout the evaluation - CtTypeReference currentType = SpoonUtil.getExpressionType(ctExpression).clone(); + CtTypeReference currentType = ExpressionUtil.getExpressionType(ctExpression).clone(); CtExpression result = evaluator.evaluate(ctExpression); assertEquals(expected, result.toString()); - assertEquals(currentType, SpoonUtil.getExpressionType(result)); + assertEquals(currentType, ExpressionUtil.getExpressionType(result)); } /** diff --git a/autograder-extra/pom.xml b/autograder-extra/pom.xml index 0aa1e916..b73749b8 100644 --- a/autograder-extra/pom.xml +++ b/autograder-extra/pom.xml @@ -16,7 +16,7 @@ 3.3.0 - 7.0.0-rc3 + 7.3.0 diff --git a/autograder-extra/src/main/java/de/firemage/autograder/extra/check/complexity/RegexCheck.java b/autograder-extra/src/main/java/de/firemage/autograder/extra/check/complexity/RegexCheck.java index ffdeca92..97f23642 100644 --- a/autograder-extra/src/main/java/de/firemage/autograder/extra/check/complexity/RegexCheck.java +++ b/autograder-extra/src/main/java/de/firemage/autograder/extra/check/complexity/RegexCheck.java @@ -4,8 +4,10 @@ import de.firemage.autograder.core.ProblemType; import de.firemage.autograder.core.check.ExecutableCheck; import de.firemage.autograder.core.integrated.IntegratedCheck; -import de.firemage.autograder.core.integrated.SpoonUtil; +import de.firemage.autograder.core.integrated.VariableUtil; import de.firemage.autograder.core.integrated.StaticAnalysis; +import de.firemage.autograder.core.integrated.MethodUtil; +import de.firemage.autograder.core.integrated.TypeUtil; import de.firemage.autograder.core.integrated.UsesFinder; import de.firemage.autograder.treeg.InvalidRegExSyntaxException; import de.firemage.autograder.treeg.RegExParser; @@ -60,22 +62,22 @@ private static boolean isRegexInvocation(CtInvocation ctInvocation) { } return ctInvocation.getTarget() instanceof CtTypeAccess ctTypeAccess - && SpoonUtil.isTypeEqualTo(ctTypeAccess.getAccessedType(), java.util.regex.Pattern.class) + && TypeUtil.isTypeEqualTo(ctTypeAccess.getAccessedType(), java.util.regex.Pattern.class) && List.of("matches", "compile").contains(ctExecutable.getSimpleName()) - || SpoonUtil.isTypeEqualTo(ctInvocation.getTarget().getType(), java.lang.String.class) + || TypeUtil.isTypeEqualTo(ctInvocation.getTarget().getType(), java.lang.String.class) && ( - SpoonUtil.isSignatureEqualTo(ctExecutable, boolean.class, "matches", String.class) - || SpoonUtil.isSignatureEqualTo(ctExecutable, String.class, "replaceAll", String.class, String.class) - || SpoonUtil.isSignatureEqualTo(ctExecutable, String.class, "replaceFirst", String.class, String.class) - || SpoonUtil.isSignatureEqualTo(ctExecutable, String[].class, "split", String.class) - || SpoonUtil.isSignatureEqualTo(ctExecutable, String[].class, "split", String.class, int.class) + MethodUtil.isSignatureEqualTo(ctExecutable, boolean.class, "matches", String.class) + || MethodUtil.isSignatureEqualTo(ctExecutable, String.class, "replaceAll", String.class, String.class) + || MethodUtil.isSignatureEqualTo(ctExecutable, String.class, "replaceFirst", String.class, String.class) + || MethodUtil.isSignatureEqualTo(ctExecutable, String[].class, "split", String.class) + || MethodUtil.isSignatureEqualTo(ctExecutable, String[].class, "split", String.class, int.class) ); } private static boolean isInAllowedContext(CtLiteral ctLiteral) { CtElement parent = ctLiteral.getParent(); if (parent instanceof CtVariable ctVariable - && SpoonUtil.isEffectivelyFinal(ctVariable)) { + && VariableUtil.isEffectivelyFinal(ctVariable)) { // Check if the variable is only used in a regex invocation (e.g. Pattern.compile) return UsesFinder.variableUses(ctVariable) .hasAnyAndAllMatch(ctVariableAccess -> ctVariableAccess.getParent() instanceof CtInvocation ctInvocation @@ -90,7 +92,7 @@ protected void check(StaticAnalysis staticAnalysis) { staticAnalysis.processWith(new AbstractProcessor>() { @Override public void process(CtLiteral literal) { - if (!SpoonUtil.isString(literal.getType()) || !isInAllowedContext(literal)) { + if (!TypeUtil.isString(literal.getType()) || !isInAllowedContext(literal)) { return; } diff --git a/autograder-extra/src/main/java/de/firemage/autograder/extra/check/general/ConstantNamingAndQualifierCheck.java b/autograder-extra/src/main/java/de/firemage/autograder/extra/check/general/ConstantNamingAndQualifierCheck.java index 34430a48..d796336b 100644 --- a/autograder-extra/src/main/java/de/firemage/autograder/extra/check/general/ConstantNamingAndQualifierCheck.java +++ b/autograder-extra/src/main/java/de/firemage/autograder/extra/check/general/ConstantNamingAndQualifierCheck.java @@ -4,9 +4,11 @@ import de.firemage.autograder.core.ProblemType; import de.firemage.autograder.core.check.ExecutableCheck; +import de.firemage.autograder.core.integrated.ExpressionUtil; +import de.firemage.autograder.core.integrated.TypeUtil; import de.firemage.autograder.extra.integrated.IdentifierNameUtils; import de.firemage.autograder.core.integrated.IntegratedCheck; -import de.firemage.autograder.core.integrated.SpoonUtil; +import de.firemage.autograder.core.integrated.VariableUtil; import de.firemage.autograder.core.integrated.StaticAnalysis; import spoon.processing.AbstractProcessor; import spoon.reflect.code.CtLiteral; @@ -59,7 +61,7 @@ public void process(CtVariable ctVariable) { // skip non-constant variables (and those that should be ignored) if (ctVariable.isImplicit() || !ctVariable.getPosition().isValidPosition() - || !SpoonUtil.isEffectivelyFinal(ctVariable) + || !VariableUtil.isEffectivelyFinal(ctVariable) || ctVariable.getDefaultExpression() == null || IGNORE_FIELDS.contains(ctVariable.getSimpleName())) { return; @@ -67,12 +69,12 @@ public void process(CtVariable ctVariable) { // only check primitive types and strings, because other types may be mutable like list // and should therefore not be static, even if they are final - if (!ctVariable.getType().unbox().isPrimitive() && !SpoonUtil.isString(ctVariable.getType())) { + if (!ctVariable.getType().unbox().isPrimitive() && !TypeUtil.isString(ctVariable.getType())) { return; } if (ctVariable instanceof CtLocalVariable ctLocalVariable - && SpoonUtil.resolveCtExpression(ctLocalVariable.getDefaultExpression()) instanceof CtLiteral) { + && ExpressionUtil.resolveCtExpression(ctLocalVariable.getDefaultExpression()) instanceof CtLiteral) { // by the check above, ctLocalVariable has a default expression and is effectively final // // this code catches the case where one tries to bypass the checkstyle by doing: diff --git a/autograder-extra/src/main/java/de/firemage/autograder/extra/check/naming/LinguisticNamingCheck.java b/autograder-extra/src/main/java/de/firemage/autograder/extra/check/naming/LinguisticNamingCheck.java index 372c12e5..4391a229 100644 --- a/autograder-extra/src/main/java/de/firemage/autograder/extra/check/naming/LinguisticNamingCheck.java +++ b/autograder-extra/src/main/java/de/firemage/autograder/extra/check/naming/LinguisticNamingCheck.java @@ -6,11 +6,14 @@ import de.firemage.autograder.core.ProblemType; import de.firemage.autograder.core.check.ExecutableCheck; +import de.firemage.autograder.core.integrated.CoreUtil; +import de.firemage.autograder.core.integrated.ExpressionUtil; +import de.firemage.autograder.core.integrated.StatementUtil; import de.firemage.autograder.extra.integrated.IdentifierNameUtils; import de.firemage.autograder.core.integrated.IntegratedCheck; import de.firemage.autograder.core.integrated.MethodHierarchy; -import de.firemage.autograder.core.integrated.SpoonUtil; import de.firemage.autograder.core.integrated.StaticAnalysis; +import de.firemage.autograder.core.integrated.TypeUtil; import spoon.processing.AbstractProcessor; import spoon.reflect.code.CtLocalVariable; import spoon.reflect.declaration.CtField; @@ -51,7 +54,7 @@ private void reportProblem(String key, CtNamedElement ctNamedElement, Map void checkCtVariable(CtVariable ctVariable) { return; } - if (hasBooleanPrefix(ctVariable) && !SpoonUtil.isBoolean(ctVariable)) { + if (hasBooleanPrefix(ctVariable) && !ExpressionUtil.isBoolean(ctVariable)) { this.reportProblem( "linguistic-naming-boolean", ctVariable, @@ -77,7 +80,7 @@ private void checkCtMethod(CtMethod ctMethod, CodeModel model) { return; } - if (hasBooleanPrefix(ctMethod) && !SpoonUtil.isBoolean(ctMethod)) { + if (hasBooleanPrefix(ctMethod) && !ExpressionUtil.isBoolean(ctMethod)) { this.reportProblem( "linguistic-naming-boolean", ctMethod, @@ -91,7 +94,7 @@ private void checkCtMethod(CtMethod ctMethod, CodeModel model) { String prefix = words.get(0); - if (prefix.equals("get") && SpoonUtil.isTypeEqualTo(ctMethod.getType(), void.class)) { + if (prefix.equals("get") && TypeUtil.isTypeEqualTo(ctMethod.getType(), void.class)) { // it is expected that a getter returns something this.reportProblem( "linguistic-naming-getter", @@ -105,7 +108,7 @@ private void checkCtMethod(CtMethod ctMethod, CodeModel model) { return; } - if (prefix.equals("set") && isInvalidSetterReturnType(ctMethod) && SpoonUtil.getEffectiveStatements(ctMethod.getBody()).size() <= 3) { + if (prefix.equals("set") && isInvalidSetterReturnType(ctMethod) && StatementUtil.getEffectiveStatements(ctMethod.getBody()).size() <= 3) { // it is expected that a setter returns nothing (void) this.reportProblem( "linguistic-naming-setter", @@ -118,7 +121,7 @@ private static boolean isInvalidSetterReturnType(CtMethod ctMethod) { CtTypeReference methodType = ctMethod.getType(); // the expected return type of a setter is void - if (SpoonUtil.isTypeEqualTo(methodType, void.class)) { + if (TypeUtil.isTypeEqualTo(methodType, void.class)) { return false; } @@ -133,7 +136,7 @@ private static boolean isInvalidSetterReturnType(CtMethod ctMethod) { } // returning a boolean is allowed as well (for indicating success) - return !SpoonUtil.isBoolean(ctMethod); + return !ExpressionUtil.isBoolean(ctMethod); } @Override diff --git a/autograder-extra/src/main/java/de/firemage/autograder/extra/check/naming/VariablesHaveDescriptiveNamesCheck.java b/autograder-extra/src/main/java/de/firemage/autograder/extra/check/naming/VariablesHaveDescriptiveNamesCheck.java index 5dbe3919..026c37f0 100644 --- a/autograder-extra/src/main/java/de/firemage/autograder/extra/check/naming/VariablesHaveDescriptiveNamesCheck.java +++ b/autograder-extra/src/main/java/de/firemage/autograder/extra/check/naming/VariablesHaveDescriptiveNamesCheck.java @@ -4,10 +4,11 @@ import de.firemage.autograder.core.ProblemType; import de.firemage.autograder.core.check.ExecutableCheck; +import de.firemage.autograder.core.integrated.TypeUtil; import de.firemage.autograder.extra.integrated.IdentifierNameUtils; import de.firemage.autograder.core.integrated.IntegratedCheck; -import de.firemage.autograder.core.integrated.SpoonUtil; import de.firemage.autograder.core.integrated.StaticAnalysis; +import de.firemage.autograder.core.integrated.MethodUtil; import org.apache.commons.lang3.StringUtils; import spoon.processing.AbstractProcessor; import spoon.reflect.code.CtCatchVariable; @@ -127,7 +128,7 @@ private static boolean isAllowedLoopCounter(CtVariable variable) { } // for (int i = 0; i < 10; i++) - return SpoonUtil.isPrimitiveNumeric(variable.getType()); + return TypeUtil.isPrimitiveNumeric(variable.getType()); } private static boolean isAbbreviation(CtVariable variable) { @@ -280,7 +281,7 @@ public void process(CtVariable ctVariable) { return; } - if (SpoonUtil.isInOverridingMethod(ctVariable)) { + if (MethodUtil.isInOverridingMethod(ctVariable)) { // The parameter of the equals and compareTo methods may be named "o", "obj", ... // // skip all overridden methods for consistency diff --git a/autograder-extra/src/main/java/de/firemage/autograder/extra/check/oop/InheritanceBadPractices.java b/autograder-extra/src/main/java/de/firemage/autograder/extra/check/oop/InheritanceBadPractices.java index a49a48f2..1a9adf29 100644 --- a/autograder-extra/src/main/java/de/firemage/autograder/extra/check/oop/InheritanceBadPractices.java +++ b/autograder-extra/src/main/java/de/firemage/autograder/extra/check/oop/InheritanceBadPractices.java @@ -6,8 +6,8 @@ import de.firemage.autograder.extra.integrated.IdentifierNameUtils; import de.firemage.autograder.core.integrated.IntegratedCheck; -import de.firemage.autograder.core.integrated.SpoonUtil; import de.firemage.autograder.core.integrated.StaticAnalysis; +import de.firemage.autograder.core.integrated.CoreUtil; import de.firemage.autograder.core.integrated.UsesFinder; import spoon.processing.AbstractProcessor; import spoon.reflect.declaration.CtClass; @@ -32,7 +32,7 @@ ProblemType.COMPOSITION_OVER_INHERITANCE }) public class InheritanceBadPractices extends IntegratedCheck { - private static final boolean IS_IN_DEBUG_MODE = SpoonUtil.isInJunitTest(); + private static final boolean IS_IN_DEBUG_MODE = CoreUtil.isInDebugMode(); @Override protected void check(StaticAnalysis staticAnalysis) { diff --git a/autograder-extra/src/main/java/de/firemage/autograder/extra/pmd/PMDCheck.java b/autograder-extra/src/main/java/de/firemage/autograder/extra/pmd/PMDCheck.java index 87f0c44f..69908e20 100644 --- a/autograder-extra/src/main/java/de/firemage/autograder/extra/pmd/PMDCheck.java +++ b/autograder-extra/src/main/java/de/firemage/autograder/extra/pmd/PMDCheck.java @@ -4,8 +4,8 @@ import de.firemage.autograder.core.ProblemType; import de.firemage.autograder.api.Translatable; import de.firemage.autograder.core.check.Check; -import net.sourceforge.pmd.Rule; -import net.sourceforge.pmd.RuleViolation; +import net.sourceforge.pmd.lang.rule.Rule; +import net.sourceforge.pmd.reporting.RuleViolation; import net.sourceforge.pmd.lang.Language; import net.sourceforge.pmd.lang.LanguageRegistry; diff --git a/autograder-extra/src/main/java/de/firemage/autograder/extra/pmd/PMDInCodeProblem.java b/autograder-extra/src/main/java/de/firemage/autograder/extra/pmd/PMDInCodeProblem.java index 126d67ab..9d41d96e 100644 --- a/autograder-extra/src/main/java/de/firemage/autograder/extra/pmd/PMDInCodeProblem.java +++ b/autograder-extra/src/main/java/de/firemage/autograder/extra/pmd/PMDInCodeProblem.java @@ -4,7 +4,7 @@ import de.firemage.autograder.core.Problem; import de.firemage.autograder.core.LocalizedMessage; import de.firemage.autograder.core.file.SourceInfo; -import net.sourceforge.pmd.RuleViolation; +import net.sourceforge.pmd.reporting.RuleViolation; import java.nio.file.Path; diff --git a/autograder-extra/src/main/java/de/firemage/autograder/extra/pmd/PMDLinter.java b/autograder-extra/src/main/java/de/firemage/autograder/extra/pmd/PMDLinter.java index 2cb8641f..aa8d9d1c 100644 --- a/autograder-extra/src/main/java/de/firemage/autograder/extra/pmd/PMDLinter.java +++ b/autograder-extra/src/main/java/de/firemage/autograder/extra/pmd/PMDLinter.java @@ -9,9 +9,9 @@ import de.firemage.autograder.core.file.UploadedFile; import net.sourceforge.pmd.PMDConfiguration; import net.sourceforge.pmd.PmdAnalysis; -import net.sourceforge.pmd.Rule; -import net.sourceforge.pmd.RulePriority; -import net.sourceforge.pmd.RuleSet; +import net.sourceforge.pmd.lang.rule.Rule; +import net.sourceforge.pmd.lang.rule.RulePriority; +import net.sourceforge.pmd.lang.rule.RuleSet; import net.sourceforge.pmd.lang.Language; import net.sourceforge.pmd.lang.LanguageRegistry; import net.sourceforge.pmd.lang.document.FileCollector; diff --git a/autograder-extra/src/main/java/de/firemage/autograder/extra/pmd/ProblemRenderer.java b/autograder-extra/src/main/java/de/firemage/autograder/extra/pmd/ProblemRenderer.java index 42feb429..524f6086 100644 --- a/autograder-extra/src/main/java/de/firemage/autograder/extra/pmd/ProblemRenderer.java +++ b/autograder-extra/src/main/java/de/firemage/autograder/extra/pmd/ProblemRenderer.java @@ -3,8 +3,8 @@ import de.firemage.autograder.core.Problem; import de.firemage.autograder.core.file.FileSourceInfo; import de.firemage.autograder.core.file.SourceInfo; -import net.sourceforge.pmd.Report; -import net.sourceforge.pmd.RuleViolation; +import net.sourceforge.pmd.reporting.Report; +import net.sourceforge.pmd.reporting.RuleViolation; import net.sourceforge.pmd.renderers.AbstractIncrementingRenderer; import org.apache.commons.io.output.NullWriter; import org.slf4j.Logger; diff --git a/autograder-extra/src/test/java/de/firemage/autograder/extra/check/TestLocalizedStrings.java b/autograder-extra/src/test/java/de/firemage/autograder/extra/check/TestLocalizedStrings.java index 2910f8e5..e9e2b63a 100644 --- a/autograder-extra/src/test/java/de/firemage/autograder/extra/check/TestLocalizedStrings.java +++ b/autograder-extra/src/test/java/de/firemage/autograder/extra/check/TestLocalizedStrings.java @@ -2,9 +2,10 @@ import de.firemage.autograder.core.Linter; import de.firemage.autograder.api.JavaVersion; -import de.firemage.autograder.core.integrated.SpoonUtil; +import de.firemage.autograder.core.integrated.ExpressionUtil; import de.firemage.autograder.extra.check.naming.LinguisticNamingCheck; import de.firemage.autograder.extra.check.naming.VariablesHaveDescriptiveNamesCheck; +import de.firemage.autograder.core.integrated.TypeUtil; import fluent.bundle.FluentBundle; import fluent.bundle.FluentResource; import fluent.syntax.AST.Message; @@ -79,7 +80,7 @@ class TestLocalizedStrings { private static FluentBundle germanBundle; private static String resolveKey(CtModel ctModel, List> args) { - if (args.isEmpty() || !(SpoonUtil.resolveCtExpression(args.get(0)) instanceof CtLiteral ctLiteral + if (args.isEmpty() || !(ExpressionUtil.resolveCtExpression(args.get(0)) instanceof CtLiteral ctLiteral && ctLiteral.getValue() instanceof String key)) { throw new IllegalArgumentException("The first argument must be a string literal: " + args); } @@ -102,7 +103,7 @@ private static List getAllLocalizedKeys(CtModel ctModel) { } Optional, List>> manualMapping = MANUAL_MAPPING.entrySet().stream() - .filter(entry -> SpoonUtil.isTypeEqualTo(ctType.getReference(), entry.getKey())) + .filter(entry -> TypeUtil.isTypeEqualTo(ctType.getReference(), entry.getKey())) .findAny(); if (manualMapping.isPresent()) { @@ -128,7 +129,7 @@ private static List getAllLocalizedKeys(CtModel ctModel) { // PMD Rule#setMessage(String) calls result.addAll(ctType.filterChildren(ctElement -> ctElement instanceof CtInvocation ctInvocation && ctInvocation.getExecutable().getSimpleName().equals("setMessage") - && SpoonUtil.isTypeEqualTo(ctInvocation.getTarget().getType(), net.sourceforge.pmd.Rule.class) + && TypeUtil.isTypeEqualTo(ctInvocation.getTarget().getType(), net.sourceforge.pmd.lang.rule.Rule.class) ) .map(ctElement -> ((CtInvocation) ctElement).getArguments().get(0)) .map(key -> resolveKey(ctModel, List.of((CtExpression) key))) diff --git a/sample_config.yaml b/sample_config.yaml index c779c7db..6bf46190 100644 --- a/sample_config.yaml +++ b/sample_config.yaml @@ -123,14 +123,14 @@ problemsToReport: - CHAR_RANGE - UNUSED_CODE_ELEMENT_PRIVATE - COMMON_REIMPLEMENTATION_MAX_MIN - - COMMON_REIMPLEMENTATION_ADD_ALL + - FOR_LOOP_CAN_BE_INVOCATION - COMMON_REIMPLEMENTATION_ARRAYS_FILL - EXCEPTION_SHOULD_NEVER_BE_CAUGHT - USE_DIFFERENT_VISIBILITY_PUBLIC_FIELD - MULTI_THREADING - BINARY_OPERATOR_ON_BOOLEAN - TODO_COMMENT - - COMMON_REIMPLEMENTATION_ADD_ENUM_VALUES + - USE_ENUM_VALUES - COMMON_REIMPLEMENTATION_SUBLIST - REDUNDANT_MODIFIER_VISIBILITY_ENUM_CONSTRUCTOR - TOO_MANY_EXCEPTIONS @@ -149,7 +149,7 @@ problemsToReport: - COMMON_REIMPLEMENTATION_ITERABLE_DUPLICATES - COLLECTIONS_N_COPIES - REDUNDANT_ELSE - - COLLECTION_ADD_ALL + - SEQUENTIAL_ADD_ALL - USE_MODULO_OPERATOR - NUMBER_FORMAT_EXCEPTION_IGNORED - REDUNDANT_ASSIGNMENT