diff --git a/CHANGELOG.md b/CHANGELOG.md index ea070b554..4db90f758 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- `RedundantJump` analysis rule, which flags redundant jump statements, e.g., `Continue`, `Exit`. +- `LoopExecutingAtMostOnce` analysis rule, which flags loop statements that can execute at most once. +- **API:** `RepeatStatementNode::getGuardExpression` method. +- **API:** `RepeatStatementNode::getStatementList` method. +- **API:** `CaseStatementNode::getSelectorExpression` method. +- **API:** `CaseItemStatementNode::getStatement` method. + ### Fixed - Parsing errors where adjacent `>` and `=` tokens were wrongly interpreted as the `>=` operator. diff --git a/delphi-checks-testkit/src/main/java/au/com/integradev/delphi/checks/verifier/CheckVerifierImpl.java b/delphi-checks-testkit/src/main/java/au/com/integradev/delphi/checks/verifier/CheckVerifierImpl.java index ce3dc4f28..7e7e40cf8 100644 --- a/delphi-checks-testkit/src/main/java/au/com/integradev/delphi/checks/verifier/CheckVerifierImpl.java +++ b/delphi-checks-testkit/src/main/java/au/com/integradev/delphi/checks/verifier/CheckVerifierImpl.java @@ -49,6 +49,7 @@ import java.nio.file.StandardCopyOption; import java.util.ArrayList; import java.util.Collection; +import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.Objects; @@ -161,28 +162,74 @@ public void verifyIssues() { private static void verifyIssuesOnLinesInternal( List issues, List expectedIssues) { - List unexpectedLines = new ArrayList<>(); - List expectedLines = - expectedIssues.stream().map(IssueExpectation::getBeginLine).collect(Collectors.toList()); + List unexpectedIssues = new ArrayList<>(); + List expectations = new ArrayList<>(expectedIssues); for (Issue issue : issues) { - IssueLocation issueLocation = issue.primaryLocation(); - - TextRange textRange = issueLocation.textRange(); - if (textRange == null) { - throw new AssertionError( - String.format( - "Expected issues to be raised at line level, not at %s level", - issueLocation.inputComponent().isFile() ? "file" : "project")); + Optional expectedIssue = findIssue(issue, expectations); + if (expectedIssue.isPresent()) { + expectations.remove(expectedIssue.get()); + } else { + unexpectedIssues.add(expectationFromIssue(issue)); } + } + + assertIssueMismatchesEmpty(expectations, unexpectedIssues); + } + + private static IssueExpectation expectationFromIssue(Issue issue) { + int primaryLine = getStartingLine(issue.primaryLocation()); + List> actualLines = + issue.flows().stream() + .map( + flow -> + flow.locations().stream() + .map(location -> getStartingLine(location) - primaryLine) + .collect(Collectors.toList())) + .sorted(Comparator.comparing(list -> list.get(0))) + .collect(Collectors.toList()); + return new IssueExpectation(primaryLine, actualLines); + } + + private static Optional findIssue( + Issue issue, List expectations) { + int line = getStartingLine(issue.primaryLocation()); - Integer line = textRange.start().line(); - if (!expectedLines.remove(line)) { - unexpectedLines.add(line); + for (IssueExpectation expectation : expectations) { + if (expectation.getBeginLine() != line + || expectation.getFlowLines().size() != issue.flows().size()) { + continue; + } + List> expectedLines = + expectation.getFlowLines().stream() + .map( + offsets -> + offsets.stream().map(offset -> offset + line).collect(Collectors.toList())) + .collect(Collectors.toList()); + List> actualLines = + issue.flows().stream() + .map( + flow -> + flow.locations().stream() + .map(CheckVerifierImpl::getStartingLine) + .collect(Collectors.toList())) + .collect(Collectors.toList()); + if (expectedLines.equals(actualLines)) { + return Optional.of(expectation); } } + return Optional.empty(); + } - assertIssueMismatchesEmpty(expectedLines, unexpectedLines); + private static int getStartingLine(IssueLocation location) { + TextRange textRange = location.textRange(); + if (textRange == null) { + throw new AssertionError( + String.format( + "Expected issues to be raised at line level, not at %s level", + location.inputComponent().isFile() ? "file" : "project")); + } + return textRange.start().line(); } private void assertQuickFixes( @@ -324,17 +371,17 @@ private static boolean textEditMatches( } private static void assertIssueMismatchesEmpty( - List expectedLines, List unexpectedLines) { - if (!expectedLines.isEmpty() || !unexpectedLines.isEmpty()) { + List expectedIssues, List unexpectedIssues) { + if (!expectedIssues.isEmpty() || !unexpectedIssues.isEmpty()) { StringBuilder message = new StringBuilder("Issues were "); - if (!expectedLines.isEmpty()) { - message.append("expected at ").append(expectedLines); + if (!expectedIssues.isEmpty()) { + message.append("expected at ").append(expectedIssues); } - if (!expectedLines.isEmpty() && !unexpectedLines.isEmpty()) { + if (!expectedIssues.isEmpty() && !unexpectedIssues.isEmpty()) { message.append(", "); } - if (!unexpectedLines.isEmpty()) { - message.append("unexpected at ").append(unexpectedLines); + if (!unexpectedIssues.isEmpty()) { + message.append("unexpected at ").append(unexpectedIssues); } throw new AssertionError(message.toString()); } diff --git a/delphi-checks-testkit/src/main/java/au/com/integradev/delphi/checks/verifier/Expectations.java b/delphi-checks-testkit/src/main/java/au/com/integradev/delphi/checks/verifier/Expectations.java index 6effafb73..bf1077910 100644 --- a/delphi-checks-testkit/src/main/java/au/com/integradev/delphi/checks/verifier/Expectations.java +++ b/delphi-checks-testkit/src/main/java/au/com/integradev/delphi/checks/verifier/Expectations.java @@ -21,15 +21,21 @@ import au.com.integradev.delphi.file.DelphiFile.DelphiInputFile; import java.util.ArrayList; import java.util.Collections; +import java.util.Comparator; import java.util.List; import java.util.regex.MatchResult; +import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; import org.sonar.plugins.communitydelphi.api.token.DelphiToken; class Expectations { private static final Pattern NONCOMPLIANT_PATTERN = - Pattern.compile("(?i)^//\\s*Noncompliant(?:@([-+]\\d+))?\\b"); + Pattern.compile("(?i)^//\\s*Noncompliant(?:@([-+]\\d+))?\\b(.+)?"); + + private static final Pattern FLOW_PATTERN = Pattern.compile("\\s*\\(([^)]*)\\)"); + private static final Pattern FLOW_LINE_OFFSET_PATTERN = + Pattern.compile("\\s*,?\\s*([+-]?\\d+)\\b"); private static final Pattern QUICK_FIX_RANGE_PATTERN = Pattern.compile("^([+-]\\d+):(\\d*) to ([+-]\\d+):(\\d*)$"); @@ -128,12 +134,21 @@ private static TextEditExpectation parseFixComment(int offset, MatchResult match private static IssueExpectation parseNoncompliantComment(int beginLine, MatchResult matchResult) { String offset = matchResult.group(1); + String flows = matchResult.group(2); + + int lineOffset = parseIssueOffset(offset); + List> flowLines = parseFlows(flows); + + return new IssueExpectation(beginLine + lineOffset, flowLines); + } + + private static int parseIssueOffset(String offset) { if (offset == null) { - return new IssueExpectation(beginLine); + return 0; } try { - return new IssueExpectation(beginLine + Integer.parseInt(offset)); + return Integer.parseInt(offset); } catch (NumberFormatException e) { throw new AssertionError( String.format( @@ -141,6 +156,43 @@ private static IssueExpectation parseNoncompliantComment(int beginLine, MatchRes } } + private static List> parseFlows(String flows) { + List> flowLines = new ArrayList<>(); + if (flows == null) { + return flowLines; + } + Matcher flowMatcher = FLOW_PATTERN.matcher(flows); + while (flowMatcher.find()) { + String flow = flowMatcher.group(1); + List lines = parseFlowLines(flow); + if (!lines.isEmpty()) { + flowLines.add(lines); + } + } + flowLines.sort(Comparator.comparing(list -> list.get(0))); + return flowLines; + } + + private static List parseFlowLines(String flow) { + List lines = new ArrayList<>(); + if (flow == null) { + return lines; + } + Matcher lineMatcher = FLOW_LINE_OFFSET_PATTERN.matcher(flow); + while (lineMatcher.find()) { + String flowLineOffset = lineMatcher.group(1); + try { + lines.add(Integer.parseInt(flowLineOffset)); + } catch (NumberFormatException e) { + throw new AssertionError( + String.format( + "Failed to parse 'Noncompliant' flow line offset '%s' as an integer.", + flowLineOffset)); + } + } + return lines; + } + private static class MatchResultOnLine { private final MatchResult matchResult; private final int line; diff --git a/delphi-checks-testkit/src/main/java/au/com/integradev/delphi/checks/verifier/IssueExpectation.java b/delphi-checks-testkit/src/main/java/au/com/integradev/delphi/checks/verifier/IssueExpectation.java index 28444af79..780f522b7 100644 --- a/delphi-checks-testkit/src/main/java/au/com/integradev/delphi/checks/verifier/IssueExpectation.java +++ b/delphi-checks-testkit/src/main/java/au/com/integradev/delphi/checks/verifier/IssueExpectation.java @@ -18,14 +18,38 @@ */ package au.com.integradev.delphi.checks.verifier; +import java.util.List; +import java.util.stream.Collectors; + class IssueExpectation { private final int beginLine; + private final List> flowLines; - public IssueExpectation(int beginLine) { + public IssueExpectation(int beginLine, List> flowLines) { this.beginLine = beginLine; + this.flowLines = flowLines; } public int getBeginLine() { return beginLine; } + + public List> getFlowLines() { + return flowLines; + } + + @Override + public String toString() { + return "{" + + beginLine + + " " + + flowLines.stream() + .map( + list -> + "(" + + list.stream().map(Object::toString).collect(Collectors.joining(", ")) + + ")") + .collect(Collectors.joining(" ")) + + "}"; + } } diff --git a/delphi-checks-testkit/src/test/java/au/com/integradev/delphi/checks/verifier/CheckVerifierImplTest.java b/delphi-checks-testkit/src/test/java/au/com/integradev/delphi/checks/verifier/CheckVerifierImplTest.java index d1d121c65..a650f5c17 100644 --- a/delphi-checks-testkit/src/test/java/au/com/integradev/delphi/checks/verifier/CheckVerifierImplTest.java +++ b/delphi-checks-testkit/src/test/java/au/com/integradev/delphi/checks/verifier/CheckVerifierImplTest.java @@ -21,14 +21,29 @@ import static org.assertj.core.api.Assertions.*; import au.com.integradev.delphi.builders.DelphiTestUnitBuilder; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.junit.jupiter.api.Named; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.sonar.check.Rule; import org.sonar.plugins.communitydelphi.api.ast.ArgumentListNode; +import org.sonar.plugins.communitydelphi.api.ast.BinaryExpressionNode; import org.sonar.plugins.communitydelphi.api.ast.DelphiAst; import org.sonar.plugins.communitydelphi.api.ast.DelphiNode; +import org.sonar.plugins.communitydelphi.api.ast.IntegerLiteralNode; import org.sonar.plugins.communitydelphi.api.ast.NameReferenceNode; import org.sonar.plugins.communitydelphi.api.check.DelphiCheck; import org.sonar.plugins.communitydelphi.api.check.DelphiCheckContext; +import org.sonar.plugins.communitydelphi.api.check.DelphiCheckContext.Location; import org.sonar.plugins.communitydelphi.api.reporting.QuickFix; import org.sonar.plugins.communitydelphi.api.reporting.QuickFixEdit; import org.sonar.plugins.communitydelphi.api.symbol.declaration.TypeNameDeclaration; @@ -367,6 +382,214 @@ void testImpliedAndNotImpliedIssue() { assertThatThrownBy(verifier::verifyIssueOnProject).isInstanceOf(AssertionError.class); } + private static Stream validSecondaryLocationsCases() { + return Stream.of( + Arguments.of(Named.of("InOrder", "(1) (2) (3)"), "(-2) (-1)"), + Arguments.of(Named.of("OutOfOrder", "(3) (1) (2)"), "(-1) (-2)"), + Arguments.of(Named.of("ExtraSpaces", " ( 1 ) (2 ) ( 3)"), "(-2 ) (-1 ) ")); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("validSecondaryLocationsCases") + void testValidSecondaryLocationsOnIssue(String annotation1, String annotation2) { + Supplier verifier = + () -> + CheckVerifier.newVerifier() + .withCheck(new WillRaiseDirectionalSecondariesOnIntegersCheck()) + .onFile( + new DelphiTestUnitBuilder() + .appendDecl("const") + .appendDecl(" Foo0 = 2; // Noncompliant " + annotation1) + .appendDecl(" Foo1 = 2;") + .appendDecl(" Foo2 = 2;") + .appendDecl(" Foo3 = 2;") + .appendDecl(" Bar0 = 1;") + .appendDecl(" Bar1 = 1;") + .appendDecl(" Bar2 = 1; // Noncompliant " + annotation2)); + + assertThatCode(verifier.get()::verifyIssues).doesNotThrowAnyException(); + assertThatThrownBy(verifier.get()::verifyNoIssues).isInstanceOf(AssertionError.class); + assertThatThrownBy(verifier.get()::verifyIssueOnFile).isInstanceOf(AssertionError.class); + assertThatThrownBy(verifier.get()::verifyIssueOnProject).isInstanceOf(AssertionError.class); + } + + private static Stream invalidSecondaryLocationsCases() { + return Stream.of( + Arguments.of(Named.of("MissingLocation", "(1) (2)"), "(-2)"), + Arguments.of(Named.of("ExtraLocation", "(1) (2) (3) (0)"), "(-2) (0) (-1)"), + Arguments.of(Named.of("ErroneousFlow", "(1, 0) (2) (3)"), "(-2, 1) (-1)"), + Arguments.of(Named.of("MissingLocation", "(1) (3)"), "(-2)")); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("invalidSecondaryLocationsCases") + void testInvalidSecondaryLocationsOnIssue(String annotation1, String annotation2) { + Supplier verifier = + () -> + CheckVerifier.newVerifier() + .withCheck(new WillRaiseDirectionalSecondariesOnIntegersCheck()) + .onFile( + new DelphiTestUnitBuilder() + .appendDecl("const") + .appendDecl(" Foo0 = 2; // Noncompliant " + annotation1) + .appendDecl(" Foo1 = 2;") + .appendDecl(" Foo2 = 2;") + .appendDecl(" Foo3 = 2;") + .appendDecl(" Bar0 = 1;") + .appendDecl(" Bar1 = 1;") + .appendDecl(" Bar2 = 1; // Noncompliant " + annotation2)); + + assertThatThrownBy(verifier.get()::verifyIssues) + .hasMessageContainingAll("Issues were expected at", "unexpected at") + .isInstanceOf(AssertionError.class); + assertThatThrownBy(verifier.get()::verifyNoIssues).isInstanceOf(AssertionError.class); + assertThatThrownBy(verifier.get()::verifyIssueOnFile).isInstanceOf(AssertionError.class); + assertThatThrownBy(verifier.get()::verifyIssueOnProject).isInstanceOf(AssertionError.class); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("invalidSecondaryLocationsCases") + void testInvalidSecondaryLocationsOnIssueWithOffset(String annotation1, String annotation2) { + Supplier verifier = + () -> + CheckVerifier.newVerifier() + .withCheck(new WillRaiseDirectionalSecondariesOnIntegersCheck()) + .onFile( + new DelphiTestUnitBuilder() + .appendDecl("const") + .appendDecl(" Foo0 = 2;") + .appendDecl(" Foo1 = 2; // Noncompliant@-1 " + annotation1) + .appendDecl(" Foo2 = 2;") + .appendDecl(" Foo3 = 2;") + .appendDecl(" Bar0 = 1;") + .appendDecl(" Bar1 = 1; // Noncompliant@+1 " + annotation2) + .appendDecl(" Bar2 = 1;")); + + assertThatThrownBy(verifier.get()::verifyIssues) + .hasMessageContainingAll("Issues were expected at", "unexpected at") + .isInstanceOf(AssertionError.class); + assertThatThrownBy(verifier.get()::verifyNoIssues).isInstanceOf(AssertionError.class); + assertThatThrownBy(verifier.get()::verifyIssueOnFile).isInstanceOf(AssertionError.class); + assertThatThrownBy(verifier.get()::verifyIssueOnProject).isInstanceOf(AssertionError.class); + } + + private static Stream validFlowsLocationsCases() { + return Stream.of( + Arguments.of(Named.of("InOrder", "(-3, -1, 2, 3) (-2, 1)")), + Arguments.of(Named.of("OutOfOrder", "(-2, 1) (-3, -1, 2, 3)")), + Arguments.of(Named.of("ExtraSpaces", " ( -3, -1 ,2, 3 ) (-2 , 1)"))); + } + + @ParameterizedTest + @MethodSource("validFlowsLocationsCases") + void testValidFlowsLocations(String flows) { + Supplier verifier = + () -> + CheckVerifier.newVerifier() + .withCheck(new WillRaiseFlowOnEachIntegerLiteralForBinaryExpressionCheck()) + .onFile( + new DelphiTestUnitBuilder() + .appendDecl("const") + .appendDecl(" Foo0 = 0;") + .appendDecl(" Bar0 = 1;") + .appendDecl(" Foo1 = 0;") + .appendDecl(" Bar1 = 0 + 1; // Noncompliant " + flows) + .appendDecl(" Bar2 = 1;") + .appendDecl(" Foo2 = 0;") + .appendDecl(" Foo3 = 0;")); + + assertThatCode(verifier.get()::verifyIssues).doesNotThrowAnyException(); + assertThatThrownBy(verifier.get()::verifyNoIssues).isInstanceOf(AssertionError.class); + assertThatThrownBy(verifier.get()::verifyIssueOnFile).isInstanceOf(AssertionError.class); + assertThatThrownBy(verifier.get()::verifyIssueOnProject).isInstanceOf(AssertionError.class); + } + + @ParameterizedTest + @MethodSource("validFlowsLocationsCases") + void testValidFlowsLocationsWithOffset(String flows) { + Supplier verifier = + () -> + CheckVerifier.newVerifier() + .withCheck(new WillRaiseFlowOnEachIntegerLiteralForBinaryExpressionCheck()) + .onFile( + new DelphiTestUnitBuilder() + .appendDecl("const") + .appendDecl(" Foo0 = 0;") + .appendDecl(" Bar0 = 1;") + .appendDecl(" Foo1 = 0;") + .appendDecl(" Bar1 = 0 + 1;") + .appendDecl(" Bar2 = 1; // Noncompliant@-1 " + flows) + .appendDecl(" Foo2 = 0;") + .appendDecl(" Foo3 = 0;")); + + assertThatCode(verifier.get()::verifyIssues).doesNotThrowAnyException(); + assertThatThrownBy(verifier.get()::verifyNoIssues).isInstanceOf(AssertionError.class); + assertThatThrownBy(verifier.get()::verifyIssueOnFile).isInstanceOf(AssertionError.class); + assertThatThrownBy(verifier.get()::verifyIssueOnProject).isInstanceOf(AssertionError.class); + } + + private static Stream invalidFlowsLocationsCases() { + return Stream.of( + Arguments.of(Named.of("LocationsOutOfOrder", "(-1, 2, 3, -3) (1, -2)")), + Arguments.of(Named.of("MissingFlow", "(-3, -1, 2, 3)")), + Arguments.of(Named.of("ExtraFlow", "(-1, 2, 3, -3) (1, -2) ()")), + Arguments.of(Named.of("MissingLocation", "(-3, -1, 2) (-2)")), + Arguments.of(Named.of("EmptyFlow", "()")), + Arguments.of(Named.of("NoFlows", ""))); + } + + @ParameterizedTest + @MethodSource("invalidFlowsLocationsCases") + void testInvalidFlowsLocationsOnIssue(String annotation) { + Supplier verifier = + () -> + CheckVerifier.newVerifier() + .withCheck(new WillRaiseFlowOnEachIntegerLiteralForBinaryExpressionCheck()) + .onFile( + new DelphiTestUnitBuilder() + .appendDecl("const") + .appendDecl(" Foo0 = 0;") + .appendDecl(" Bar0 = 1;") + .appendDecl(" Foo1 = 0;") + .appendDecl(" Bar1 = 0 + 1; // Noncompliant " + annotation) + .appendDecl(" Bar2 = 1;") + .appendDecl(" Foo2 = 0;") + .appendDecl(" Foo3 = 0;")); + + assertThatThrownBy(verifier.get()::verifyIssues) + .hasMessageContainingAll("Issues were expected at", "unexpected at") + .isInstanceOf(AssertionError.class); + assertThatThrownBy(verifier.get()::verifyNoIssues).isInstanceOf(AssertionError.class); + assertThatThrownBy(verifier.get()::verifyIssueOnFile).isInstanceOf(AssertionError.class); + assertThatThrownBy(verifier.get()::verifyIssueOnProject).isInstanceOf(AssertionError.class); + } + + @ParameterizedTest + @MethodSource("invalidFlowsLocationsCases") + void testInvalidFlowsLocationsOnIssueWithOffset(String annotation) { + Supplier verifier = + () -> + CheckVerifier.newVerifier() + .withCheck(new WillRaiseFlowOnEachIntegerLiteralForBinaryExpressionCheck()) + .onFile( + new DelphiTestUnitBuilder() + .appendDecl("const") + .appendDecl(" Foo0 = 0;") + .appendDecl(" Bar0 = 1;") + .appendDecl(" Foo1 = 0; // Noncompliant@+1 " + annotation) + .appendDecl(" Bar1 = 0 + 1;") + .appendDecl(" Bar2 = 1;") + .appendDecl(" Foo2 = 0;") + .appendDecl(" Foo3 = 0;")); + + assertThatThrownBy(verifier.get()::verifyIssues) + .hasMessageContainingAll("Issues were expected at", "unexpected at") + .isInstanceOf(AssertionError.class); + assertThatThrownBy(verifier.get()::verifyNoIssues).isInstanceOf(AssertionError.class); + assertThatThrownBy(verifier.get()::verifyIssueOnFile).isInstanceOf(AssertionError.class); + assertThatThrownBy(verifier.get()::verifyIssueOnProject).isInstanceOf(AssertionError.class); + } + @Test void testFileIssue() { CheckVerifier verifier = @@ -495,6 +718,106 @@ public DelphiCheckContext visit(NameReferenceNode nameReference, DelphiCheckCont } } + /** + * This check will add secondary locations on: the same even integer after the first instance; and + * the same odd integer before the last instance. + */ + @Rule(key = "WillRaiseDirectionalSecondariesOnIntegersCheck") + public static class WillRaiseDirectionalSecondariesOnIntegersCheck extends DelphiCheck { + + private final Map> locations = new HashMap<>(); + + @Override + public DelphiCheckContext visit(DelphiAst ast, DelphiCheckContext context) { + context = super.visit(ast, context); + for (Entry> entry : locations.entrySet()) { + Integer key = entry.getKey(); + List literals = entry.getValue(); + IntegerLiteralNode node; + if (key % 2 == 0) { + node = literals.remove(0); + } else { + node = literals.remove(literals.size() - 1); + } + + context + .newIssue() + .onNode(node) + .withMessage(MESSAGE) + .withSecondaries( + literals.stream() + .map(literal -> new Location(MESSAGE, literal)) + .collect(Collectors.toList())) + .report(); + } + return context; + } + + @Override + public DelphiCheckContext visit( + IntegerLiteralNode integerLiteralNode, DelphiCheckContext context) { + Integer number = integerLiteralNode.getValue().intValue(); + if (this.locations.containsKey(number)) { + locations.get(number).add(integerLiteralNode); + } else { + List numberLocations = new ArrayList<>(); + numberLocations.add(integerLiteralNode); + locations.put(number, numberLocations); + } + + return context; + } + } + + @Rule(key = "WillRaiseFlowOnEachIntegerLiteralForBinaryExpression") + public static class WillRaiseFlowOnEachIntegerLiteralForBinaryExpressionCheck + extends DelphiCheck { + + private final List binaryExpressions = new ArrayList<>(); + private final Map> locations = new HashMap<>(); + + @Override + public DelphiCheckContext visit(DelphiAst ast, DelphiCheckContext context) { + context = super.visit(ast, context); + for (BinaryExpressionNode node : binaryExpressions) { + IntegerLiteralNode left = node.getLeft().getFirstDescendantOfType(IntegerLiteralNode.class); + IntegerLiteralNode right = + node.getRight().getFirstDescendantOfType(IntegerLiteralNode.class); + if (left == null || right == null) { + continue; + } + + List leftLocations = this.locations.get(left.getValue().intValue()); + List rightLocations = this.locations.get(right.getValue().intValue()); + context + .newIssue() + .onNode(node) + .withMessage(MESSAGE) + .withFlows(List.of(leftLocations, rightLocations)) + .report(); + } + return context; + } + + @Override + public DelphiCheckContext visit( + IntegerLiteralNode integerLiteralNode, DelphiCheckContext context) { + Integer number = integerLiteralNode.getValue().intValue(); + if (!locations.containsKey(number)) { + this.locations.put(number, new ArrayList<>()); + } + this.locations.get(number).add(new Location(MESSAGE, integerLiteralNode)); + return context; + } + + @Override + public DelphiCheckContext visit( + BinaryExpressionNode binaryExpressionNode, DelphiCheckContext context) { + this.binaryExpressions.add(binaryExpressionNode); + return context; + } + } + @Rule(key = "WillRaiseFileIssue") public static class WillRaiseFileIssueCheck extends DelphiCheck { @Override diff --git a/delphi-checks/src/main/java/au/com/integradev/delphi/checks/CheckList.java b/delphi-checks/src/main/java/au/com/integradev/delphi/checks/CheckList.java index 1d9de99ac..a619ae562 100644 --- a/delphi-checks/src/main/java/au/com/integradev/delphi/checks/CheckList.java +++ b/delphi-checks/src/main/java/au/com/integradev/delphi/checks/CheckList.java @@ -104,6 +104,7 @@ public final class CheckList { InterfaceGuidCheck.class, InterfaceNameCheck.class, LegacyInitializationSectionCheck.class, + LoopExecutingAtMostOnceCheck.class, LowercaseKeywordCheck.class, MathFunctionSingleOverloadCheck.class, MemberDeclarationOrderCheck.class, @@ -128,6 +129,7 @@ public final class CheckList { RedundantBooleanCheck.class, RedundantCastCheck.class, RedundantInheritedCheck.class, + RedundantJumpCheck.class, RedundantParenthesesCheck.class, RoutineNameCheck.class, RoutineNestingDepthCheck.class, diff --git a/delphi-checks/src/main/java/au/com/integradev/delphi/checks/LoopExecutingAtMostOnceCheck.java b/delphi-checks/src/main/java/au/com/integradev/delphi/checks/LoopExecutingAtMostOnceCheck.java new file mode 100644 index 000000000..08e28f335 --- /dev/null +++ b/delphi-checks/src/main/java/au/com/integradev/delphi/checks/LoopExecutingAtMostOnceCheck.java @@ -0,0 +1,315 @@ +/* + * Sonar Delphi Plugin + * Copyright (C) 2025 Integrated Application Development + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package au.com.integradev.delphi.checks; + +import au.com.integradev.delphi.antlr.ast.node.AnonymousMethodNodeImpl; +import au.com.integradev.delphi.antlr.ast.node.RoutineImplementationNodeImpl; +import au.com.integradev.delphi.cfg.ControlFlowGraphFactory; +import au.com.integradev.delphi.cfg.api.Block; +import au.com.integradev.delphi.cfg.api.Branch; +import au.com.integradev.delphi.cfg.api.ControlFlowGraph; +import au.com.integradev.delphi.cfg.api.Terminated; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Queue; +import java.util.Set; +import java.util.function.Supplier; +import org.sonar.check.Rule; +import org.sonar.plugins.communitydelphi.api.ast.CompoundStatementNode; +import org.sonar.plugins.communitydelphi.api.ast.DelphiAst; +import org.sonar.plugins.communitydelphi.api.ast.DelphiNode; +import org.sonar.plugins.communitydelphi.api.ast.FinalizationSectionNode; +import org.sonar.plugins.communitydelphi.api.ast.ForInStatementNode; +import org.sonar.plugins.communitydelphi.api.ast.ForStatementNode; +import org.sonar.plugins.communitydelphi.api.ast.ForToStatementNode; +import org.sonar.plugins.communitydelphi.api.ast.GotoStatementNode; +import org.sonar.plugins.communitydelphi.api.ast.IfStatementNode; +import org.sonar.plugins.communitydelphi.api.ast.InitializationSectionNode; +import org.sonar.plugins.communitydelphi.api.ast.NameReferenceNode; +import org.sonar.plugins.communitydelphi.api.ast.RaiseStatementNode; +import org.sonar.plugins.communitydelphi.api.ast.RepeatStatementNode; +import org.sonar.plugins.communitydelphi.api.ast.StatementListNode; +import org.sonar.plugins.communitydelphi.api.ast.StatementNode; +import org.sonar.plugins.communitydelphi.api.ast.WhileStatementNode; +import org.sonar.plugins.communitydelphi.api.check.DelphiCheck; +import org.sonar.plugins.communitydelphi.api.check.DelphiCheckContext; +import org.sonar.plugins.communitydelphi.api.check.DelphiCheckContext.Location; +import org.sonar.plugins.communitydelphi.api.check.FilePosition; +import org.sonar.plugins.communitydelphi.api.symbol.declaration.NameDeclaration; +import org.sonar.plugins.communitydelphi.api.symbol.declaration.RoutineNameDeclaration; + +@Rule(key = "LoopExecutingAtMostOnce") +public class LoopExecutingAtMostOnceCheck extends DelphiCheck { + private static final Set EXIT_METHODS = + Set.of("System.Exit", "System.Break", "System.Halt"); + + private final Deque loopStack = new ArrayDeque<>(); + private final Deque> violations = new ArrayDeque<>(); + + // Loops + + private void pushLoop(DelphiNode node) { + loopStack.push(node); + violations.push(new ArrayList<>()); + } + + private void popLoop(DelphiCheckContext context) { + DelphiNode loop = loopStack.pop(); + List loopViolations = violations.pop(); + if (loop == null || loopViolations == null || loopViolations.isEmpty()) { + return; + } + + context + .newIssue() + .onFilePosition(FilePosition.from(loop.getFirstToken())) + .withMessage("Remove this loop that executes only once.") + .withSecondaries(loopViolations) + .report(); + } + + @Override + public DelphiCheckContext visit(ForStatementNode node, DelphiCheckContext data) { + pushLoop(node); + DelphiCheckContext result = super.visit(node, data); + popLoop(data); + return result; + } + + @Override + public DelphiCheckContext visit(RepeatStatementNode node, DelphiCheckContext data) { + pushLoop(node); + DelphiCheckContext result = super.visit(node, data); + popLoop(data); + return result; + } + + @Override + public DelphiCheckContext visit(WhileStatementNode node, DelphiCheckContext data) { + pushLoop(node); + DelphiCheckContext result = super.visit(node, data); + popLoop(data); + return result; + } + + // Statements + + @Override + public DelphiCheckContext visit(RaiseStatementNode node, DelphiCheckContext context) { + return visitExitingNode(node, context, "raise"); + } + + @Override + public DelphiCheckContext visit(GotoStatementNode node, DelphiCheckContext context) { + return visitExitingNode(node, context, "goto"); + } + + @Override + public DelphiCheckContext visit(NameReferenceNode node, DelphiCheckContext context) { + NameDeclaration declaration = node.getLastName().getNameDeclaration(); + if (!(declaration instanceof RoutineNameDeclaration)) { + return context; + } + String fullyQualifiedName = ((RoutineNameDeclaration) declaration).fullyQualifiedName(); + if (!EXIT_METHODS.contains(fullyQualifiedName)) { + return context; + } + + return visitExitingNode(node, context, declaration.getImage()); + } + + private DelphiCheckContext visitExitingNode( + DelphiNode exitingNode, DelphiCheckContext context, String description) { + + if (isInViolatingLoop(exitingNode) && isUnconditionalJump(exitingNode)) { + List violationLocations = violations.peek(); + if (violationLocations == null) { + return context; + } + violationLocations.add( + new Location( + String.format("Remove this \"%s\" statement or make it conditional.", description), + exitingNode)); + } + return context; + } + + private boolean isInViolatingLoop(DelphiNode jump) { + DelphiNode loop = this.loopStack.peek(); + if (loop == null) { + return false; + } + ControlFlowGraph cfg = getCFG(loop); + Block loopBlock = + getTerminatorBlock(cfg, loop) + .orElseThrow( + () -> new IllegalStateException("CFG necessarily contains the loop block")); + + return !hasPredecessorInBlock(loopBlock, loop) && !jumpsBeforeLoop(cfg, loopBlock, jump); + } + + private static boolean isUnconditionalJump(DelphiNode node) { + DelphiNode lastStatement = node; + for (StatementNode statement : node.getParentsOfType(StatementNode.class)) { + if (statement instanceof ForStatementNode + || statement instanceof RepeatStatementNode + || statement instanceof WhileStatementNode) { + // Reached the loop, it is a non-conditional statement or in a chain of `else` blocks + return true; + } + + if (statement instanceof IfStatementNode + && ((IfStatementNode) statement).getElseStatement() != lastStatement) { + // If we are in the `if then` branch, then it is not relevant + return false; + } + + lastStatement = statement; + } + return false; + } + + private static Optional getTerminatorBlock(ControlFlowGraph cfg, DelphiNode terminator) { + return cfg.getBlocks().stream() + .filter(Terminated.class::isInstance) + .filter(terminated -> terminator.equals(((Terminated) terminated).getTerminator())) + .findFirst(); + } + + private static boolean hasPredecessorInBlock(Block block, DelphiNode loop) { + for (Block predecessor : block.getPredecessors()) { + List predecessorElements = predecessor.getElements(); + if (predecessorElements.isEmpty()) { + return hasPredecessorInBlock(predecessor, loop); + } + DelphiNode predecessorFirstElement = predecessorElements.get(0); + + if (isForStatementInitializer(predecessorFirstElement, loop)) { + continue; + } + + if (isDescendant(predecessorFirstElement, loop)) { + return true; + } + } + + return false; + } + + private static boolean jumpsBeforeLoop(ControlFlowGraph cfg, Block loopBlock, DelphiNode node) { + if (!(node instanceof GotoStatementNode)) { + // If the node isn't a `goto`, it cannot jump before the loop + return false; + } + Optional jumpBlock = getTerminatorBlock(cfg, node); + if (jumpBlock.isEmpty()) { + // Unable to find a block whose terminator is the `goto` + return false; + } + Block jumpTarget = jumpBlock.get().getSuccessors().iterator().next(); + if (jumpTarget == null) { + // There are no successors to the jump block + return false; + } + if (loopBlock instanceof Branch) { + Branch loopBranch = (Branch) loopBlock; + if (loopBranch.getTerminator() instanceof RepeatStatementNode + && loopBranch.getFalseBlock().equals(jumpTarget)) { + // If the jump target is the start of a `repeat` loop, it is before the loop + return true; + } + } + + // From the jump target, recursively search the successors to find the loop block. Whether the + // loop block is found relates to if the jump is to before the loop. + Set visited = new HashSet<>(); + Queue queue = new ArrayDeque<>(); + queue.add(jumpTarget); + while (!queue.isEmpty()) { + Block search = queue.poll(); + if (search.equals(loopBlock)) { + return true; + } + if ((search.getSuccessors().size() == 1 && search.getSuccessors().contains(jumpTarget)) + || search.equals(cfg.getExitBlock())) { + return false; + } + + visited.add(search); + search.getSuccessors().stream().filter(b -> !visited.contains(b)).forEach(queue::add); + } + + return false; + } + + private static boolean isForStatementInitializer(DelphiNode lastElement, DelphiNode loop) { + if (loop instanceof ForToStatementNode) { + return isDescendant(lastElement, ((ForToStatementNode) loop).getInitializerExpression()) + || isDescendant(lastElement, ((ForToStatementNode) loop).getTargetExpression()); + } + return loop instanceof ForInStatementNode + && isDescendant(lastElement, ((ForInStatementNode) loop).getEnumerable()); + } + + private static boolean isDescendant(DelphiNode descendant, DelphiNode target) { + DelphiNode parent = descendant; + while (parent != null) { + if (parent.equals(target)) { + return true; + } + parent = parent.getParent(); + } + return false; + } + + private static Supplier getCFGSupplier(DelphiNode node) { + if (node instanceof RoutineImplementationNodeImpl) { + return ((RoutineImplementationNodeImpl) node)::getControlFlowGraph; + } + if (node instanceof AnonymousMethodNodeImpl) { + return ((AnonymousMethodNodeImpl) node)::getControlFlowGraph; + } + if (node instanceof CompoundStatementNode && node.getParent() instanceof DelphiAst) { + return () -> ControlFlowGraphFactory.create((CompoundStatementNode) node); + } + if (node instanceof StatementListNode + && (node.getParent() instanceof InitializationSectionNode + || node.getParent() instanceof FinalizationSectionNode)) { + return () -> ControlFlowGraphFactory.create((StatementListNode) node); + } + return null; + } + + private static ControlFlowGraph getCFG(DelphiNode loop) { + DelphiNode parent = loop.getParent(); + Supplier cfgSupplier = getCFGSupplier(parent); + while (parent != null && cfgSupplier == null) { + parent = parent.getParent(); + cfgSupplier = getCFGSupplier(parent); + } + if (cfgSupplier != null) { + return cfgSupplier.get(); + } + return ControlFlowGraphFactory.create(loop.findChildrenOfType(StatementNode.class)); + } +} diff --git a/delphi-checks/src/main/java/au/com/integradev/delphi/checks/RedundantJumpCheck.java b/delphi-checks/src/main/java/au/com/integradev/delphi/checks/RedundantJumpCheck.java new file mode 100644 index 000000000..ff10a6300 --- /dev/null +++ b/delphi-checks/src/main/java/au/com/integradev/delphi/checks/RedundantJumpCheck.java @@ -0,0 +1,161 @@ +/* + * Sonar Delphi Plugin + * Copyright (C) 2025 Integrated Application Development + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package au.com.integradev.delphi.checks; + +import au.com.integradev.delphi.antlr.ast.node.RoutineImplementationNodeImpl; +import au.com.integradev.delphi.cfg.ControlFlowGraphFactory; +import au.com.integradev.delphi.cfg.api.Block; +import au.com.integradev.delphi.cfg.api.ControlFlowGraph; +import au.com.integradev.delphi.cfg.api.Finally; +import au.com.integradev.delphi.cfg.api.Linear; +import au.com.integradev.delphi.cfg.api.UnconditionalJump; +import org.sonar.check.Rule; +import org.sonar.plugins.communitydelphi.api.ast.AnonymousMethodNode; +import org.sonar.plugins.communitydelphi.api.ast.ArgumentListNode; +import org.sonar.plugins.communitydelphi.api.ast.CompoundStatementNode; +import org.sonar.plugins.communitydelphi.api.ast.DelphiNode; +import org.sonar.plugins.communitydelphi.api.ast.NameReferenceNode; +import org.sonar.plugins.communitydelphi.api.ast.RoutineImplementationNode; +import org.sonar.plugins.communitydelphi.api.check.DelphiCheck; +import org.sonar.plugins.communitydelphi.api.check.DelphiCheckContext; +import org.sonar.plugins.communitydelphi.api.symbol.declaration.NameDeclaration; +import org.sonar.plugins.communitydelphi.api.symbol.declaration.RoutineNameDeclaration; + +@Rule(key = "RedundantJump") +public class RedundantJumpCheck extends DelphiCheck { + private static final String MESSAGE = "Remove this redundant jump."; + + @Override + public DelphiCheckContext visit(RoutineImplementationNode routine, DelphiCheckContext context) { + ControlFlowGraph cfg = ((RoutineImplementationNodeImpl) routine).getControlFlowGraph(); + if (cfg != null) { + cfg.getBlocks().forEach(block -> checkBlock(block, context)); + } + + return super.visit(routine, context); + } + + @Override + public DelphiCheckContext visit(AnonymousMethodNode routine, DelphiCheckContext context) { + CompoundStatementNode compoundStatementNode = + routine.getFirstChildOfType(CompoundStatementNode.class); + if (compoundStatementNode != null) { + ControlFlowGraph cfg = ControlFlowGraphFactory.create(compoundStatementNode); + cfg.getBlocks().forEach(block -> checkBlock(block, context)); + } + + return super.visit(routine, context); + } + + private void checkBlock(Block block, DelphiCheckContext context) { + if (!(block instanceof UnconditionalJump)) { + return; + } + + UnconditionalJump jump = (UnconditionalJump) block; + Block successorWithoutJump = jump.getSuccessorIfRemoved(); + DelphiNode terminator = jump.getTerminator(); + + RoutineNameDeclaration routineNameDeclaration = getRoutineNameDeclaration(terminator); + if (!isContinueOrExit(routineNameDeclaration) + || isExitWithExpression(routineNameDeclaration, terminator)) { + return; + } + + Block successor = jump.getSuccessor(); + successorWithoutJump = nonEmptySuccessor(successorWithoutJump); + + if (!successorWithoutJump.equals(successor)) { + return; + } + + Block finallyBlock = getFinallyBlock(block); + if (finallyBlock != null) { + if (onlyFinallyBlocksBeforeEnd(finallyBlock)) { + reportIssue(context, terminator, MESSAGE); + } + return; + } + + reportIssue(context, terminator, MESSAGE); + } + + private static Block getFinallyBlock(Block block) { + return block.getSuccessors().stream() + .filter(Finally.class::isInstance) + .findFirst() + .orElse(null); + } + + private static boolean onlyFinallyBlocksBeforeEnd(Block finallyBlock) { + while (finallyBlock.getSuccessors().size() == 1) { + Block finallySuccessor = finallyBlock.getSuccessors().iterator().next(); + if (!(finallySuccessor instanceof Finally)) { + break; + } + finallyBlock = finallySuccessor; + } + return finallyBlock.getSuccessors().size() == 1 + && finallyBlock.getSuccessors().iterator().next().getSuccessors().isEmpty(); + } + + private static RoutineNameDeclaration getRoutineNameDeclaration(DelphiNode node) { + if (!(node instanceof NameReferenceNode)) { + return null; + } + NameDeclaration nameDeclaration = ((NameReferenceNode) node).getNameDeclaration(); + if (!(nameDeclaration instanceof RoutineNameDeclaration)) { + return null; + } + return (RoutineNameDeclaration) nameDeclaration; + } + + private static boolean isContinueOrExit(RoutineNameDeclaration routineNameDeclaration) { + if (routineNameDeclaration == null) { + return false; + } + String fullyQualifiedName = routineNameDeclaration.fullyQualifiedName(); + return fullyQualifiedName.equals("System.Continue") || fullyQualifiedName.equals("System.Exit"); + } + + private static boolean isExitWithExpression( + RoutineNameDeclaration routineNameDeclaration, DelphiNode statement) { + if (routineNameDeclaration == null) { + return false; + } + String fullyQualifiedName = routineNameDeclaration.fullyQualifiedName(); + if (!fullyQualifiedName.equals("System.Exit")) { + return false; + } + ArgumentListNode argumentList = + statement.getParent().getFirstChildOfType(ArgumentListNode.class); + if (argumentList == null) { + return false; + } + return argumentList.getArgumentNodes().size() == 1; + } + + private static Block nonEmptySuccessor(Block initialBlock) { + Block result = initialBlock; + while (result.getElements().isEmpty() && result instanceof Linear) { + result = ((Linear) result).getSuccessor(); + } + return result; + } +} diff --git a/delphi-checks/src/main/resources/org/sonar/l10n/delphi/rules/community-delphi/LoopExecutingAtMostOnce.html b/delphi-checks/src/main/resources/org/sonar/l10n/delphi/rules/community-delphi/LoopExecutingAtMostOnce.html new file mode 100644 index 000000000..5afcc73ec --- /dev/null +++ b/delphi-checks/src/main/resources/org/sonar/l10n/delphi/rules/community-delphi/LoopExecutingAtMostOnce.html @@ -0,0 +1,63 @@ +

Why is this an issue?

+

+ Loops with at most one iteration are equivalent to an if statement. Using loops in + this case makes the code less readable. +

+

+ If the intention was to execute the loop once, an if statement may be used or the + loop removed. + Otherwise, the jumping statement should be made conditional so the loop can execute more than + once. +

+

+ Loops with at most one iteration can happen with a statement that unconditionally transfers + control is misplaced inside the body of the loop. +

+

+ These statements are: +

+
    +
  • Exit
  • +
  • Break
  • +
  • Halt
  • +
  • raise
  • +
  • goto
  • +
+ +

How to fix it

+

+ Make the statement that affects execution of the loop conditional, or remove it all together. +

+
+var I := 0;
+while I < 10 do begin
+  Inc(I);
+  Break; // Noncompliant
+end;
+
+
+for var I := 0 to 10 do begin
+  if I = 2 then
+    Break // Noncompliant
+  else begin
+    Writeln(I);
+    Exit; // Noncompliant
+  end;
+end;
+
+

Compliant solution

+
+var I := 0;
+while I < 10 do begin
+  Inc(I);
+end;
+
+
+for var I := 0 to 10 do begin
+  if I = 2 then
+    Break
+  else begin
+    Writeln(I);
+  end;
+end;
+
diff --git a/delphi-checks/src/main/resources/org/sonar/l10n/delphi/rules/community-delphi/LoopExecutingAtMostOnce.json b/delphi-checks/src/main/resources/org/sonar/l10n/delphi/rules/community-delphi/LoopExecutingAtMostOnce.json new file mode 100644 index 000000000..dbe2f9a2d --- /dev/null +++ b/delphi-checks/src/main/resources/org/sonar/l10n/delphi/rules/community-delphi/LoopExecutingAtMostOnce.json @@ -0,0 +1,19 @@ +{ + "title": "Loops with at most one iteration should be refactored", + "type": "CODE_SMELL", + "status": "ready", + "remediation": { + "func": "Constant/Issue", + "constantCost": "5min" + }, + "code": { + "attribute": "CLEAR", + "impacts": { + "RELIABILITY": "MEDIUM" + } + }, + "tags": ["clumsy"], + "defaultSeverity": "Major", + "scope": "ALL", + "quickfix": "unknown" +} diff --git a/delphi-checks/src/main/resources/org/sonar/l10n/delphi/rules/community-delphi/RedundantJump.html b/delphi-checks/src/main/resources/org/sonar/l10n/delphi/rules/community-delphi/RedundantJump.html new file mode 100644 index 000000000..0e3c12381 --- /dev/null +++ b/delphi-checks/src/main/resources/org/sonar/l10n/delphi/rules/community-delphi/RedundantJump.html @@ -0,0 +1,26 @@ +

Why is this an issue?

+

+ Unnecessarily annotating the default control flow of a program using statements such as + Continue, Break, and Exit makes the code harder to read and + understand. Although these statements appear to alter the flow of a program, the control flow is + identical without it. +

+

How to fix it

+

Remove the redundant jump:

+
+procedure Example;
+begin
+  while Condition do begin
+    // ...
+    Continue; // Noncompliant
+  end;
+end;
+
+
+procedure Example;
+begin
+  while Condition do begin
+    // ...
+  end;
+end;
+
\ No newline at end of file diff --git a/delphi-checks/src/main/resources/org/sonar/l10n/delphi/rules/community-delphi/RedundantJump.json b/delphi-checks/src/main/resources/org/sonar/l10n/delphi/rules/community-delphi/RedundantJump.json new file mode 100644 index 000000000..49c18b0af --- /dev/null +++ b/delphi-checks/src/main/resources/org/sonar/l10n/delphi/rules/community-delphi/RedundantJump.json @@ -0,0 +1,19 @@ +{ + "title": "Redundant jumps should not be used", + "type": "CODE_SMELL", + "status": "ready", + "remediation": { + "func": "Constant/Issue", + "constantCost": "2min" + }, + "code": { + "attribute": "CLEAR", + "impacts": { + "MAINTAINABILITY": "MEDIUM" + } + }, + "tags": ["clumsy"], + "defaultSeverity": "Minor", + "scope": "ALL", + "quickfix": "unknown" +} diff --git a/delphi-checks/src/main/resources/org/sonar/l10n/delphi/rules/community-delphi/Sonar_way_profile.json b/delphi-checks/src/main/resources/org/sonar/l10n/delphi/rules/community-delphi/Sonar_way_profile.json index 6c31c4c6a..246e0380e 100644 --- a/delphi-checks/src/main/resources/org/sonar/l10n/delphi/rules/community-delphi/Sonar_way_profile.json +++ b/delphi-checks/src/main/resources/org/sonar/l10n/delphi/rules/community-delphi/Sonar_way_profile.json @@ -49,6 +49,7 @@ "InstanceInvokedConstructor", "InterfaceName", "LegacyInitializationSection", + "LoopExecutingAtMostOnce", "LowercaseKeyword", "MathFunctionSingleOverload", "MemberDeclarationOrder", @@ -70,6 +71,7 @@ "RedundantBoolean", "RedundantCast", "RedundantInherited", + "RedundantJump", "RedundantParentheses", "RoutineName", "RoutineNestingDepth", diff --git a/delphi-checks/src/test/java/au/com/integradev/delphi/checks/CheckTestNameTest.java b/delphi-checks/src/test/java/au/com/integradev/delphi/checks/CheckTestNameTest.java index 79ef23378..5d959c613 100644 --- a/delphi-checks/src/test/java/au/com/integradev/delphi/checks/CheckTestNameTest.java +++ b/delphi-checks/src/test/java/au/com/integradev/delphi/checks/CheckTestNameTest.java @@ -28,6 +28,7 @@ import com.tngtech.archunit.core.domain.JavaMember; import com.tngtech.archunit.core.domain.JavaMethod; import com.tngtech.archunit.core.domain.JavaModifier; +import com.tngtech.archunit.core.domain.properties.CanBeAnnotated.Predicates; import com.tngtech.archunit.core.domain.properties.HasName; import com.tngtech.archunit.core.importer.ClassFileImporter; import com.tngtech.archunit.lang.ArchCondition; @@ -35,6 +36,7 @@ import com.tngtech.archunit.lang.SimpleConditionEvent; import java.util.List; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; import org.sonar.plugins.communitydelphi.api.check.DelphiCheck; class CheckTestNameTest { @@ -123,6 +125,10 @@ void testCheckTestsVerifyingIssuesAreNamedCorrectly() { methods() .that(VERIFY_ISSUES) .and(not(TESTING_IMPLEMENTATION_DETAILS)) + .and( + DescribedPredicate.or( + Predicates.annotatedWith(Test.class), + Predicates.annotatedWith(ParameterizedTest.class))) .should() .haveNameMatching(".*ShouldAdd(Issues?|QuickFix(es)?)$") .allowEmptyShould(true) @@ -135,6 +141,10 @@ void testCheckTestsVerifyingNoIssuesAreNamedCorrectly() { .that(VERIFY_NO_ISSUES) .and(not(TESTING_IMPLEMENTATION_DETAILS)) .and(not(CALL_ASSERT_THROW_BY)) + .and( + DescribedPredicate.or( + Predicates.annotatedWith(Test.class), + Predicates.annotatedWith(ParameterizedTest.class))) .should() .haveNameMatching(".*ShouldNotAddIssues?$") .allowEmptyShould(true) @@ -166,8 +176,10 @@ void testCheckTestsShouldNotThrowAreNamedCorrectly() { @Test void testCheckTestsShouldBeNamedCorrectly() { methods() - .that() - .areAnnotatedWith(Test.class) + .that( + DescribedPredicate.or( + Predicates.annotatedWith(Test.class), + Predicates.annotatedWith(ParameterizedTest.class))) .and(not(DECLARED_IN_METATESTS)) .should() .haveNameMatching(".*Should((Not)?(Throw|Add(Issues?|QuickFix(es)?)))") diff --git a/delphi-checks/src/test/java/au/com/integradev/delphi/checks/LoopExecutingAtMostOnceCheckTest.java b/delphi-checks/src/test/java/au/com/integradev/delphi/checks/LoopExecutingAtMostOnceCheckTest.java new file mode 100644 index 000000000..3136136af --- /dev/null +++ b/delphi-checks/src/test/java/au/com/integradev/delphi/checks/LoopExecutingAtMostOnceCheckTest.java @@ -0,0 +1,626 @@ +/* + * Sonar Delphi Plugin + * Copyright (C) 2025 Integrated Application Development + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package au.com.integradev.delphi.checks; + +import static java.lang.String.format; + +import au.com.integradev.delphi.builders.DelphiTestProgramBuilder; +import au.com.integradev.delphi.builders.DelphiTestUnitBuilder; +import au.com.integradev.delphi.checks.verifier.CheckVerifier; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +class LoopExecutingAtMostOnceCheckTest { + + enum LoopType { + WHILE("while A do begin", "end;"), + FOR_IN("for var A in B do begin", "end;"), + FOR_TO("for var A := B to C do begin", "end;"), + FOR_DOWNTO("for var A := B downto C do begin", "end;"), + REPEAT("repeat", "until A = B;"); + + final String loopHeader; + final String loopFooter; + + LoopType(String loopHeader, String loopFooter) { + this.loopHeader = loopHeader; + this.loopFooter = loopFooter; + } + } + + // Continue + @ParameterizedTest + @EnumSource(value = LoopType.class) + void testUnconditionalContinueShouldNotAddIssue(LoopType loopType) { + DelphiTestUnitBuilder unitBuilder = + new DelphiTestUnitBuilder() + .appendImpl("procedure Test;") + .appendImpl("begin") + .appendImpl(format(" %s // Compliant", loopType.loopHeader)) + .appendImpl(" Continue; // Compliant") + .appendImpl(format(" %s", loopType.loopFooter)) + .appendImpl("end;"); + + CheckVerifier.newVerifier() + .withCheck(new LoopExecutingAtMostOnceCheck()) + .onFile(unitBuilder) + .verifyNoIssues(); + } + + // Break + @ParameterizedTest + @EnumSource(value = LoopType.class) + void testUnconditionalBreakShouldAddIssue(LoopType loopType) { + DelphiTestUnitBuilder unitBuilder = + new DelphiTestUnitBuilder() + .appendImpl("procedure Test;") + .appendImpl("begin") + .appendImpl(format(" %s // Noncompliant (1)", loopType.loopHeader)) + .appendImpl(" Break; // Secondary") + .appendImpl(format(" %s", loopType.loopFooter)) + .appendImpl("end;"); + + CheckVerifier.newVerifier() + .withCheck(new LoopExecutingAtMostOnceCheck()) + .onFile(unitBuilder) + .verifyIssues(); + } + + @ParameterizedTest + @EnumSource(value = LoopType.class) + void testConditionalBreakShouldNotAddIssue(LoopType loopType) { + DelphiTestUnitBuilder unitBuilder = + new DelphiTestUnitBuilder() + .appendImpl("procedure Test;") + .appendImpl("begin") + .appendImpl(format(" %s // Compliant", loopType.loopHeader)) + .appendImpl(" if A then") + .appendImpl(" Break; // Compliant") + .appendImpl(format(" %s", loopType.loopFooter)) + .appendImpl("end;"); + + CheckVerifier.newVerifier() + .withCheck(new LoopExecutingAtMostOnceCheck()) + .onFile(unitBuilder) + .verifyNoIssues(); + } + + // Exit + @ParameterizedTest + @EnumSource(value = LoopType.class) + void testUnconditionalExitShouldAddIssue(LoopType loopType) { + DelphiTestUnitBuilder unitBuilder = + new DelphiTestUnitBuilder() + .appendImpl("procedure Test;") + .appendImpl("begin") + .appendImpl(format(" %s // Noncompliant (1)", loopType.loopHeader)) + .appendImpl(" Exit; // Secondary") + .appendImpl(format(" %s", loopType.loopFooter)) + .appendImpl("end;"); + + CheckVerifier.newVerifier() + .withCheck(new LoopExecutingAtMostOnceCheck()) + .onFile(unitBuilder) + .verifyIssues(); + } + + @ParameterizedTest + @EnumSource(value = LoopType.class) + void testConditionalExitShouldNotAddIssue(LoopType loopType) { + DelphiTestUnitBuilder unitBuilder = + new DelphiTestUnitBuilder() + .appendImpl("procedure Test;") + .appendImpl("begin") + .appendImpl(format(" %s // Compliant", loopType.loopHeader)) + .appendImpl(" if A then") + .appendImpl(" Exit; // Compliant") + .appendImpl(format(" %s", loopType.loopFooter)) + .appendImpl("end;"); + + CheckVerifier.newVerifier() + .withCheck(new LoopExecutingAtMostOnceCheck()) + .onFile(unitBuilder) + .verifyNoIssues(); + } + + // Halt + @ParameterizedTest + @EnumSource(value = LoopType.class) + void testUnconditionalHaltShouldAddIssue(LoopType loopType) { + DelphiTestUnitBuilder unitBuilder = + new DelphiTestUnitBuilder() + .appendImpl("procedure Test;") + .appendImpl("begin") + .appendImpl(format(" %s // Noncompliant (1)", loopType.loopHeader)) + .appendImpl(" Halt; // Secondary") + .appendImpl(format(" %s", loopType.loopFooter)) + .appendImpl("end;"); + + CheckVerifier.newVerifier() + .withCheck(new LoopExecutingAtMostOnceCheck()) + .onFile(unitBuilder) + .verifyIssues(); + } + + @ParameterizedTest + @EnumSource(value = LoopType.class) + void testConditionalHaltShouldNotAddIssue(LoopType loopType) { + DelphiTestUnitBuilder unitBuilder = + new DelphiTestUnitBuilder() + .appendImpl("procedure Test;") + .appendImpl("begin") + .appendImpl(format(" %s // Compliant", loopType.loopHeader)) + .appendImpl(" if A then") + .appendImpl(" Halt; // Compliant") + .appendImpl(format(" %s", loopType.loopFooter)) + .appendImpl("end;"); + + CheckVerifier.newVerifier() + .withCheck(new LoopExecutingAtMostOnceCheck()) + .onFile(unitBuilder) + .verifyNoIssues(); + } + + // Raise + @ParameterizedTest + @EnumSource(value = LoopType.class) + void testUnconditionalRaiseShouldAddIssue(LoopType loopType) { + DelphiTestUnitBuilder unitBuilder = + new DelphiTestUnitBuilder() + .appendImpl("procedure Test;") + .appendImpl("begin") + .appendImpl(format(" %s // Noncompliant (1)", loopType.loopHeader)) + .appendImpl(" raise E; // Secondary") + .appendImpl(format(" %s", loopType.loopFooter)) + .appendImpl("end;"); + + CheckVerifier.newVerifier() + .withCheck(new LoopExecutingAtMostOnceCheck()) + .onFile(unitBuilder) + .verifyIssues(); + } + + @ParameterizedTest + @EnumSource(value = LoopType.class) + void testConditionalRaiseShouldNotAddIssue(LoopType loopType) { + DelphiTestUnitBuilder unitBuilder = + new DelphiTestUnitBuilder() + .appendImpl("procedure Test;") + .appendImpl("begin") + .appendImpl(format(" %s // Compliant", loopType.loopHeader)) + .appendImpl(" if A then") + .appendImpl(" raise B; // Compliant") + .appendImpl(format(" %s", loopType.loopFooter)) + .appendImpl("end;"); + + CheckVerifier.newVerifier() + .withCheck(new LoopExecutingAtMostOnceCheck()) + .onFile(unitBuilder) + .verifyNoIssues(); + } + + // Goto + @ParameterizedTest + @EnumSource(value = LoopType.class) + void testUnconditionalGotoBeforeShouldNotAddIssue(LoopType loopType) { + DelphiTestUnitBuilder unitBuilder = + new DelphiTestUnitBuilder() + .appendImpl("procedure Test;") + .appendImpl(" label before;") + .appendImpl("begin") + .appendImpl(" before:") + .appendImpl(format(" %s // Compliant", loopType.loopHeader)) + .appendImpl(" goto before; // Compliant") + .appendImpl(format(" %s", loopType.loopFooter)) + .appendImpl("end;"); + + CheckVerifier.newVerifier() + .withCheck(new LoopExecutingAtMostOnceCheck()) + .onFile(unitBuilder) + .verifyNoIssues(); + } + + @ParameterizedTest + @EnumSource(value = LoopType.class) + void testUnconditionalGotoAfterShouldAddIssue(LoopType loopType) { + DelphiTestUnitBuilder unitBuilder = + new DelphiTestUnitBuilder() + .appendImpl("procedure Test;") + .appendImpl(" label after;") + .appendImpl("begin") + .appendImpl(format(" %s // Noncompliant (1)", loopType.loopHeader)) + .appendImpl(" goto after; // Secondary") + .appendImpl(format(" %s", loopType.loopFooter)) + .appendImpl(" after:") + .appendImpl("end;"); + + CheckVerifier.newVerifier() + .withCheck(new LoopExecutingAtMostOnceCheck()) + .onFile(unitBuilder) + .verifyIssues(); + } + + @ParameterizedTest + @EnumSource(value = LoopType.class) + void testConditionalGotoShouldNotAddIssue(LoopType loopType) { + DelphiTestUnitBuilder unitBuilder = + new DelphiTestUnitBuilder() + .appendImpl("procedure Test;") + .appendImpl(" label before;") + .appendImpl("begin") + .appendImpl(" before:") + .appendImpl(format(" %s // Compliant", loopType.loopHeader)) + .appendImpl(" if A then") + .appendImpl(" goto before; // Compliant") + .appendImpl(format(" %s", loopType.loopFooter)) + .appendImpl("end;"); + + CheckVerifier.newVerifier() + .withCheck(new LoopExecutingAtMostOnceCheck()) + .onFile(unitBuilder) + .verifyNoIssues(); + } + + @ParameterizedTest + @EnumSource(value = LoopType.class) + void testGotoBeforeExitShouldAddIssue(LoopType loopType) { + DelphiTestUnitBuilder unitBuilder = + new DelphiTestUnitBuilder() + .appendImpl("procedure Test;") + .appendImpl(" label before;") + .appendImpl("begin") + .appendImpl(" before:") + .appendImpl(" Exit;") + .appendImpl(format(" %s // Noncompliant (1)", loopType.loopHeader)) + .appendImpl(" goto before; // Secondary") + .appendImpl(format(" %s", loopType.loopFooter)) + .appendImpl("end;"); + + CheckVerifier.newVerifier() + .withCheck(new LoopExecutingAtMostOnceCheck()) + .onFile(unitBuilder) + .verifyIssues(); + } + + @ParameterizedTest + @EnumSource(value = LoopType.class) + void testGotoMultiBlockInfiniteLoopShouldAddIssue(LoopType loopType) { + DelphiTestUnitBuilder unitBuilder = + new DelphiTestUnitBuilder() + .appendImpl("procedure Test;") + .appendImpl(" label before, middle;") + .appendImpl("begin") + .appendImpl(" before:") + .appendImpl(" Writeln('A');") + .appendImpl(" middle:") + .appendImpl(" goto before;") + .appendImpl(format(" %s // Noncompliant (1)", loopType.loopHeader)) + .appendImpl(" goto middle; // Secondary") + .appendImpl(format(" %s", loopType.loopFooter)) + .appendImpl("end;"); + + CheckVerifier.newVerifier() + .withCheck(new LoopExecutingAtMostOnceCheck()) + .onFile(unitBuilder) + .verifyIssues(); + } + + @ParameterizedTest + @EnumSource(value = LoopType.class) + void testGotoSameBlockInfiniteLoopShouldAddIssue(LoopType loopType) { + DelphiTestUnitBuilder unitBuilder = + new DelphiTestUnitBuilder() + .appendImpl("procedure Test;") + .appendImpl(" label before;") + .appendImpl("begin") + .appendImpl(" before:") + .appendImpl(" goto before;") + .appendImpl(format(" %s // Noncompliant (1)", loopType.loopHeader)) + .appendImpl(" goto before; // Secondary") + .appendImpl(format(" %s", loopType.loopFooter)) + .appendImpl("end;"); + + CheckVerifier.newVerifier() + .withCheck(new LoopExecutingAtMostOnceCheck()) + .onFile(unitBuilder) + .verifyIssues(); + } + + // Mixed + @Test + void testIfBreakElseExitShouldAddIssue() { + DelphiTestUnitBuilder unitBuilder = + new DelphiTestUnitBuilder() + .appendImpl("procedure Test;") + .appendImpl("begin") + .appendImpl(" while A do begin // Noncompliant (4)") + .appendImpl(" if A then") + .appendImpl(" Break // Compliant") + .appendImpl(" else") + .appendImpl(" Exit; // Secondary") + .appendImpl(" end;") + .appendImpl("end;"); + + CheckVerifier.newVerifier() + .withCheck(new LoopExecutingAtMostOnceCheck()) + .onFile(unitBuilder) + .verifyIssues(); + } + + @Test + void testIfBreakElseIfExitShouldNotAddIssue() { + DelphiTestUnitBuilder unitBuilder = + new DelphiTestUnitBuilder() + .appendImpl("procedure Test;") + .appendImpl("begin") + .appendImpl(" while A do begin // Compliant") + .appendImpl(" if A then") + .appendImpl(" Break // Compliant") + .appendImpl(" else if B then") + .appendImpl(" Exit; // Compliant") + .appendImpl(" end;") + .appendImpl("end;"); + + CheckVerifier.newVerifier() + .withCheck(new LoopExecutingAtMostOnceCheck()) + .onFile(unitBuilder) + .verifyNoIssues(); + } + + @Test + void testIfExitElseIfBreakThenExitShouldAddIssue() { + DelphiTestUnitBuilder unitBuilder = + new DelphiTestUnitBuilder() + .appendImpl("procedure Test;") + .appendImpl("begin") + .appendImpl(" while A do begin // Noncompliant (5)") + .appendImpl(" if A then") + .appendImpl(" Exit // Compliant") + .appendImpl(" else if B then") + .appendImpl(" Break // Compliant") + .appendImpl(" Exit; // Secondary") + .appendImpl(" end;") + .appendImpl("end;"); + + CheckVerifier.newVerifier() + .withCheck(new LoopExecutingAtMostOnceCheck()) + .onFile(unitBuilder) + .verifyIssues(); + } + + @Test + void testIfContinueElseExitShouldNotAddIssue() { + DelphiTestUnitBuilder unitBuilder = + new DelphiTestUnitBuilder() + .appendImpl("procedure Test;") + .appendImpl("begin") + .appendImpl(" while A do begin // Compliant") + .appendImpl(" if A then") + .appendImpl(" Continue // Compliant") + .appendImpl(" else") + .appendImpl(" Exit; // Compliant") + .appendImpl(" end;") + .appendImpl("end;"); + + CheckVerifier.newVerifier() + .withCheck(new LoopExecutingAtMostOnceCheck()) + .onFile(unitBuilder) + .verifyNoIssues(); + } + + @Test + void testIfNestedShouldNotAddIssue() { + DelphiTestUnitBuilder unitBuilder = + new DelphiTestUnitBuilder() + .appendImpl("procedure Test;") + .appendImpl("begin") + .appendImpl(" while A do begin // Compliant") + .appendImpl(" if A then begin") + .appendImpl(" if B then") + .appendImpl(" Break // Compliant") + .appendImpl(" else") + .appendImpl(" Exit; // Compliant") + .appendImpl(" end;") + .appendImpl(" end;") + .appendImpl("end;"); + + CheckVerifier.newVerifier() + .withCheck(new LoopExecutingAtMostOnceCheck()) + .onFile(unitBuilder) + .verifyNoIssues(); + } + + @Test + void testElseNestedShouldAddIssue() { + DelphiTestUnitBuilder unitBuilder = + new DelphiTestUnitBuilder() + .appendImpl("procedure Test;") + .appendImpl("begin") + .appendImpl(" while A do begin // Noncompliant (7)") + .appendImpl(" if A then begin") + .appendImpl(" Break // Compliant") + .appendImpl(" end else begin") + .appendImpl(" if B then") + .appendImpl(" Break // Compliant") + .appendImpl(" else") + .appendImpl(" Exit; // Secondary") + .appendImpl(" end;") + .appendImpl(" end;") + .appendImpl("end;"); + + CheckVerifier.newVerifier() + .withCheck(new LoopExecutingAtMostOnceCheck()) + .onFile(unitBuilder) + .verifyIssues(); + } + + @Test + void testConditionalBreakAndUnconditionalExitShouldAddIssue() { + DelphiTestUnitBuilder unitBuilder = + new DelphiTestUnitBuilder() + .appendImpl("procedure Test;") + .appendImpl("begin") + .appendImpl(" while A do begin // Noncompliant (3)") + .appendImpl(" if B then") + .appendImpl(" Break; // Compliant") + .appendImpl(" Exit; // Secondary") + .appendImpl(" end;") + .appendImpl("end;"); + + CheckVerifier.newVerifier() + .withCheck(new LoopExecutingAtMostOnceCheck()) + .onFile(unitBuilder) + .verifyIssues(); + } + + @Test + void testIfExitElseBreakAndUnconditionalBreakShouldAddIssues() { + DelphiTestUnitBuilder unitBuilder = + new DelphiTestUnitBuilder() + .appendImpl("procedure Test;") + .appendImpl("begin") + .appendImpl(" while A do begin // Noncompliant (4) (5)") + .appendImpl(" if A then") + .appendImpl(" Exit // Compliant") + .appendImpl(" else") + .appendImpl(" Break; // Secondary") + .appendImpl(" Exit; // Secondary") + .appendImpl(" end;") + .appendImpl("end;"); + + CheckVerifier.newVerifier() + .withCheck(new LoopExecutingAtMostOnceCheck()) + .onFile(unitBuilder) + .verifyIssues(); + } + + @Test + void testInnerNestedLoopViolationShouldAddIssue() { + DelphiTestUnitBuilder unitBuilder = + new DelphiTestUnitBuilder() + .appendImpl("procedure Test;") + .appendImpl("begin") + .appendImpl(" while A do begin // Compliant") + .appendImpl(" while A do begin // Noncompliant (4)") + .appendImpl(" if A then") + .appendImpl(" Exit // Compliant") + .appendImpl(" else") + .appendImpl(" Break; // Secondary") + .appendImpl(" end;") + .appendImpl(" end;") + .appendImpl("end;"); + + CheckVerifier.newVerifier() + .withCheck(new LoopExecutingAtMostOnceCheck()) + .onFile(unitBuilder) + .verifyIssues(); + } + + @Test + void testOuterNestedLoopViolationShouldAddIssue() { + DelphiTestUnitBuilder unitBuilder = + new DelphiTestUnitBuilder() + .appendImpl("procedure Test;") + .appendImpl("begin") + .appendImpl(" while A do begin // Noncompliant (7)") + .appendImpl(" while A do begin // Noncompliant (4)") + .appendImpl(" if A then") + .appendImpl(" Exit // Compliant") + .appendImpl(" else") + .appendImpl(" Exit; // Inner secondary") + .appendImpl(" end;") + .appendImpl(" Break; // Outer secondary") + .appendImpl(" end;") + .appendImpl("end;"); + + CheckVerifier.newVerifier() + .withCheck(new LoopExecutingAtMostOnceCheck()) + .onFile(unitBuilder) + .verifyIssues(); + } + + @ParameterizedTest + @EnumSource(value = LoopType.class) + void testCfgInProgramShouldAddIssue(LoopType loopType) { + var programBuilder = + new DelphiTestProgramBuilder() + .appendImpl("A := True;") + .appendImpl(format("%s // Noncompliant (1)", loopType.loopHeader)) + .appendImpl(" Break;") + .appendImpl(format("%s", loopType.loopFooter)); + + CheckVerifier.newVerifier() + .withCheck(new LoopExecutingAtMostOnceCheck()) + .onFile(programBuilder) + .verifyIssues(); + } + + @ParameterizedTest + @EnumSource(value = LoopType.class) + void testCfgInInitializationShouldAddIssue(LoopType loopType) { + var unitBuilder = + new DelphiTestUnitBuilder() + .appendImpl("initialization") + .appendImpl(" A := True;") + .appendImpl(format(" %s // Noncompliant (1)", loopType.loopHeader)) + .appendImpl(" Break;") + .appendImpl(format(" %s", loopType.loopFooter)); + + CheckVerifier.newVerifier() + .withCheck(new LoopExecutingAtMostOnceCheck()) + .onFile(unitBuilder) + .verifyIssues(); + } + + @ParameterizedTest + @EnumSource(value = LoopType.class) + void testCfgInUnitBeginShouldAddIssue(LoopType loopType) { + var unitBuilder = + new DelphiTestUnitBuilder() + .appendImpl("begin") + .appendImpl(" A := True;") + .appendImpl(format(" %s // Noncompliant (1)", loopType.loopHeader)) + .appendImpl(" Break;") + .appendImpl(format(" %s", loopType.loopFooter)); + + CheckVerifier.newVerifier() + .withCheck(new LoopExecutingAtMostOnceCheck()) + .onFile(unitBuilder) + .verifyIssues(); + } + + @ParameterizedTest + @EnumSource(value = LoopType.class) + void testCfgInFinalizationShouldAddIssue(LoopType loopType) { + var unitBuilder = + new DelphiTestUnitBuilder() + .appendImpl("initialization") + .appendImpl("finalization") + .appendImpl(" A := True;") + .appendImpl(format(" %s // Noncompliant (1)", loopType.loopHeader)) + .appendImpl(" Break;") + .appendImpl(format(" %s", loopType.loopFooter)); + + CheckVerifier.newVerifier() + .withCheck(new LoopExecutingAtMostOnceCheck()) + .onFile(unitBuilder) + .verifyIssues(); + } +} diff --git a/delphi-checks/src/test/java/au/com/integradev/delphi/checks/RedundantJumpCheckTest.java b/delphi-checks/src/test/java/au/com/integradev/delphi/checks/RedundantJumpCheckTest.java new file mode 100644 index 000000000..d710c83b8 --- /dev/null +++ b/delphi-checks/src/test/java/au/com/integradev/delphi/checks/RedundantJumpCheckTest.java @@ -0,0 +1,356 @@ +/* + * Sonar Delphi Plugin + * Copyright (C) 2025 Integrated Application Development + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package au.com.integradev.delphi.checks; + +import static java.lang.String.format; + +import au.com.integradev.delphi.builders.DelphiTestUnitBuilder; +import au.com.integradev.delphi.checks.verifier.CheckVerifier; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +class RedundantJumpCheckTest { + + enum LoopType { + WHILE("while A do begin", "end;"), + FOR_IN("for var A in B do begin", "end;"), + FOR_TO("for var A := B to C do begin", "end;"), + FOR_DOWNTO("for var A := B downto C do begin", "end;"), + REPEAT("repeat", "until A = B;"); + + final String loopHeader; + final String loopFooter; + + LoopType(String loopHeader, String loopFooter) { + this.loopHeader = loopHeader; + this.loopFooter = loopFooter; + } + } + + private void doLoopTest(LoopType loopType, List loopContents, boolean expectIssues) { + var unitBuilder = + new DelphiTestUnitBuilder() + .appendImpl("procedure Test;") + .appendImpl("begin") + .appendImpl(format(" %s", loopType.loopHeader)); + for (String loopLine : loopContents) { + unitBuilder.appendImpl(" " + loopLine); + } + unitBuilder.appendImpl(format(" %s", loopType.loopFooter)).appendImpl("end;"); + + var verifier = + CheckVerifier.newVerifier().withCheck(new RedundantJumpCheck()).onFile(unitBuilder); + if (expectIssues) { + verifier.verifyIssues(); + } else { + verifier.verifyNoIssues(); + } + } + + private void doExitTest(List functionContents, boolean expectIssues) { + var unitBuilder = + new DelphiTestUnitBuilder() + .appendImpl("function A: Boolean; begin end;") + .appendImpl("function B: Boolean; begin end;") + .appendImpl("function C: Boolean; begin end;") + .appendImpl("function Test: Integer;") + .appendImpl("begin"); + for (String loopLine : functionContents) { + unitBuilder.appendImpl(" " + loopLine); + } + unitBuilder.appendImpl("end;"); + + var verifier = + CheckVerifier.newVerifier().withCheck(new RedundantJumpCheck()).onFile(unitBuilder); + if (expectIssues) { + verifier.verifyIssues(); + } else { + verifier.verifyNoIssues(); + } + } + + // Continue + @ParameterizedTest + @EnumSource(value = LoopType.class) + void testLoopBareContinueShouldAddIssue(LoopType loopType) { + doLoopTest(loopType, List.of("Continue; // Noncompliant"), true); + } + + @ParameterizedTest + @EnumSource(value = LoopType.class) + void testLoopIfElseContinuesShouldAddIssues(LoopType loopType) { + doLoopTest( + loopType, + List.of("if Foo then Continue // Noncompliant", "else Continue; // Noncompliant"), + true); + } + + @ParameterizedTest + @EnumSource(value = LoopType.class) + void testLoopConditionalContinueShouldAddIssue(LoopType loopType) { + doLoopTest(loopType, List.of("if Foo then Bar else Continue; // Noncompliant"), true); + } + + @ParameterizedTest + @EnumSource(value = LoopType.class) + void testLoopContinueBeforeEndShouldNotAddIssue(LoopType loopType) { + doLoopTest(loopType, List.of("Continue;", "Bar;"), false); + } + + @ParameterizedTest + @EnumSource(value = LoopType.class) + void testLoopConditionalContinueBeforeEndShouldNotAddIssues(LoopType loopType) { + doLoopTest(loopType, List.of("if Foo then Continue;", "Bar;"), false); + } + + @ParameterizedTest + @EnumSource(value = LoopType.class) + void testLoopContinueAtEndShouldAddIssue(LoopType loopType) { + doLoopTest(loopType, List.of("Bar;", "Continue; // Noncompliant"), true); + } + + @ParameterizedTest + @EnumSource(value = LoopType.class) + void testLoopConditionalContinueAtEndShouldAddIssue(LoopType loopType) { + doLoopTest(loopType, List.of("Bar;", "if Foo then Continue; // Noncompliant"), true); + } + + // Break + @ParameterizedTest + @EnumSource(value = LoopType.class) + void testLoopBareBreakShouldNotAddIssue(LoopType loopType) { + doLoopTest(loopType, List.of("Break;"), false); + } + + @ParameterizedTest + @EnumSource(value = LoopType.class) + void testLoopConditionalBreakShouldNotAddIssue(LoopType loopType) { + doLoopTest(loopType, List.of("if Foo then Bar else Break;"), false); + } + + @ParameterizedTest + @EnumSource(value = LoopType.class) + void testLoopBreakBeforeEndShouldNotAddIssue(LoopType loopType) { + doLoopTest(loopType, List.of("Break;", "Bar;"), false); + } + + @ParameterizedTest + @EnumSource(value = LoopType.class) + void testLoopConditionalBreakBeforeEndShouldNotAddIssue(LoopType loopType) { + doLoopTest(loopType, List.of("if Foo then Break;", "Bar;"), false); + } + + @ParameterizedTest + @EnumSource(value = LoopType.class) + void testLoopBreakAtEndShouldNotAddIssue(LoopType loopType) { + doLoopTest(loopType, List.of("Bar;", "Break;"), false); + } + + @ParameterizedTest + @EnumSource(value = LoopType.class) + void testLoopConditionalBreakAtEndShouldAddIssue(LoopType loopType) { + doLoopTest(loopType, List.of("Bar;", "if Foo then Continue; // Noncompliant"), true); + } + + // Exit + @Test + void testExitWithValueAtEndShouldNotAddIssue() { + doExitTest(List.of("Foo;", "Exit(42);"), false); + } + + @Test + void testExitWithValueBeforeEndShouldNotAddIssue() { + doExitTest(List.of("Exit(42);", "Foo;"), false); + } + + @Test + void testExitShouldAddIssue() { + doExitTest(List.of("Exit; // Noncompliant"), true); + } + + @Test + void testExitAtEndShouldAddIssue() { + doExitTest(List.of("Foo;", "Exit; // Noncompliant"), true); + } + + @Test + void testExitBeforeEndShouldNotAddIssue() { + doExitTest(List.of("Exit; // Noncompliant", "Foo;"), false); + } + + @Test + void testConditionalExitBeforeEndShouldNotAddIssue() { + doExitTest(List.of("if Bar then Exit;", "Foo;"), false); + } + + @Test + void testExitInTryFinallyBeforeEndShouldNotAddIssue() { + doExitTest( + List.of("try", " if True then Exit;", "finally", " Foo1;", "end;", "Foo2;"), false); + } + + @Test + void testExitInNestedTryFinallyBeforeEndShouldNotAddIssue() { + doExitTest( + List.of( + "try", + " try", + " if True then Exit;", + " finally", + " Foo1;", + " end;", + "finally", + " Foo2;", + "end;", + "Foo3;"), + false); + } + + @Test + void testExitInNestedTryFinallyBeforeInnerEndShouldNotAddIssue() { + doExitTest( + List.of( + "try", + " try", + " if True then Exit;", + " finally", + " Foo1;", + " end;", + " Foo2;", + "finally", + " Foo3;", + "end;"), + false); + } + + @Test + void testExitInTryFinallyAtEndShouldAddIssue() { + doExitTest( + List.of("try", " if True then Exit; // Noncompliant", "finally", " Foo1;", "end;"), true); + } + + @Test + void testExitInNestedTryFinallyAtEndShouldAddIssue() { + doExitTest( + List.of( + "try", + " try", + " if True then Exit; // Noncompliant", + " finally", + " Foo1;", + " end;", + "finally", + " Foo2;", + "end;"), + true); + } + + @Test + void testExitInNestedTryExceptFinallyExceptBeforeEndShouldNotAddIssue() { + doExitTest( + List.of( + "try", + " try", + " Exit;", + " except", + " Foo1;", + " end;", + "finally", + " Foo2;", + "end;", + "Foo;"), + false); + } + + @Test + void testConditionalExitThenRaiseShouldNotAddIssues() { + doExitTest( + List.of( + "try", + " A;", + "except", + " on E: Exception do begin", + " if B then Exit;", + " raise;", + " end;", + "end;", + "C;"), + false); + } + + @Test + void testExceptExitShouldNotAddIssue() { + doExitTest( + List.of( + "try", + " try", + " A;", + " except", + " on E: Exception do begin", + " Exit;", + " end;", + " end;", + " B;", + "finally;", + " C;", + "end;"), + false); + } + + @Test + void testForLoopAfterConditionalTryFinallyExitShouldNotAddIssue() { + doExitTest( + List.of( + "try", + " A;", + " if B then begin Exit end;", + "finally", + " C;", + "end;", + "for var I in [1..2] do A;"), + false); + } + + @Test + void testConditionalExitInForLoopTryFinallyShouldNotAddIssue() { + doExitTest( + List.of( + "for var I in [1..2] do begin", + " A;", + " try", + " B;", + " if C then begin", + " Exit", + " end;", + " finally", + " A;", + " end;", + "end;", + "B;"), + false); + } + + @Test + void testAnonymousMethodExitShouldAddIssue() { + doExitTest( + List.of("var A := procedure", " begin", " Exit; // Noncompliant", " end;"), true); + } +} diff --git a/delphi-frontend/src/main/java/au/com/integradev/delphi/antlr/ast/node/AnonymousMethodNodeImpl.java b/delphi-frontend/src/main/java/au/com/integradev/delphi/antlr/ast/node/AnonymousMethodNodeImpl.java index 6239a8b72..8d2aa5453 100644 --- a/delphi-frontend/src/main/java/au/com/integradev/delphi/antlr/ast/node/AnonymousMethodNodeImpl.java +++ b/delphi-frontend/src/main/java/au/com/integradev/delphi/antlr/ast/node/AnonymousMethodNodeImpl.java @@ -19,8 +19,12 @@ package au.com.integradev.delphi.antlr.ast.node; import au.com.integradev.delphi.antlr.ast.visitors.DelphiParserVisitor; +import au.com.integradev.delphi.cfg.ControlFlowGraphFactory; +import au.com.integradev.delphi.cfg.api.ControlFlowGraph; import au.com.integradev.delphi.type.factory.TypeFactoryImpl; import au.com.integradev.delphi.type.parameter.FormalParameter; +import com.google.common.base.Supplier; +import com.google.common.base.Suppliers; import java.util.Collections; import java.util.Set; import java.util.stream.Collectors; @@ -42,6 +46,16 @@ public final class AnonymousMethodNodeImpl extends ExpressionNodeImpl implements AnonymousMethodNode { private String image; + private final Supplier cfgSupplier = + Suppliers.memoize( + () -> { + CompoundStatementNode compoundStatementNode = + getFirstChildOfType(CompoundStatementNode.class); + if (compoundStatementNode == null) { + return null; + } + return ControlFlowGraphFactory.create(compoundStatementNode); + }); public AnonymousMethodNodeImpl(Token token) { super(token); @@ -154,4 +168,9 @@ protected Type createType() { : returnTypeNode.getTypeNode().getType(), getDirectives()); } + + @Nullable + public ControlFlowGraph getControlFlowGraph() { + return cfgSupplier.get(); + } } diff --git a/delphi-frontend/src/main/java/au/com/integradev/delphi/antlr/ast/node/CaseItemStatementNodeImpl.java b/delphi-frontend/src/main/java/au/com/integradev/delphi/antlr/ast/node/CaseItemStatementNodeImpl.java index 5f2d45d0d..efc813f3b 100644 --- a/delphi-frontend/src/main/java/au/com/integradev/delphi/antlr/ast/node/CaseItemStatementNodeImpl.java +++ b/delphi-frontend/src/main/java/au/com/integradev/delphi/antlr/ast/node/CaseItemStatementNodeImpl.java @@ -20,9 +20,11 @@ import au.com.integradev.delphi.antlr.ast.visitors.DelphiParserVisitor; import java.util.List; +import javax.annotation.Nullable; import org.antlr.runtime.Token; import org.sonar.plugins.communitydelphi.api.ast.CaseItemStatementNode; import org.sonar.plugins.communitydelphi.api.ast.ExpressionNode; +import org.sonar.plugins.communitydelphi.api.ast.StatementNode; public final class CaseItemStatementNodeImpl extends DelphiNodeImpl implements CaseItemStatementNode { @@ -43,4 +45,10 @@ public T accept(DelphiParserVisitor visitor, T data) { public List getExpressions() { return findChildrenOfType(ExpressionNode.class); } + + @Nullable + @Override + public StatementNode getStatement() { + return getFirstChildOfType(StatementNode.class); + } } diff --git a/delphi-frontend/src/main/java/au/com/integradev/delphi/antlr/ast/node/CaseStatementNodeImpl.java b/delphi-frontend/src/main/java/au/com/integradev/delphi/antlr/ast/node/CaseStatementNodeImpl.java index 353f358f0..b855c12b0 100644 --- a/delphi-frontend/src/main/java/au/com/integradev/delphi/antlr/ast/node/CaseStatementNodeImpl.java +++ b/delphi-frontend/src/main/java/au/com/integradev/delphi/antlr/ast/node/CaseStatementNodeImpl.java @@ -25,12 +25,18 @@ import org.sonar.plugins.communitydelphi.api.ast.CaseItemStatementNode; import org.sonar.plugins.communitydelphi.api.ast.CaseStatementNode; import org.sonar.plugins.communitydelphi.api.ast.ElseBlockNode; +import org.sonar.plugins.communitydelphi.api.ast.ExpressionNode; public final class CaseStatementNodeImpl extends DelphiNodeImpl implements CaseStatementNode { public CaseStatementNodeImpl(Token token) { super(token); } + @Override + public ExpressionNode getSelectorExpression() { + return getFirstChildOfType(ExpressionNode.class); + } + @Override public List getCaseItems() { return findChildrenOfType(CaseItemStatementNode.class); diff --git a/delphi-frontend/src/main/java/au/com/integradev/delphi/antlr/ast/node/RepeatStatementNodeImpl.java b/delphi-frontend/src/main/java/au/com/integradev/delphi/antlr/ast/node/RepeatStatementNodeImpl.java index 847d6faaa..f10121f9f 100644 --- a/delphi-frontend/src/main/java/au/com/integradev/delphi/antlr/ast/node/RepeatStatementNodeImpl.java +++ b/delphi-frontend/src/main/java/au/com/integradev/delphi/antlr/ast/node/RepeatStatementNodeImpl.java @@ -20,13 +20,25 @@ import au.com.integradev.delphi.antlr.ast.visitors.DelphiParserVisitor; import org.antlr.runtime.Token; +import org.sonar.plugins.communitydelphi.api.ast.ExpressionNode; import org.sonar.plugins.communitydelphi.api.ast.RepeatStatementNode; +import org.sonar.plugins.communitydelphi.api.ast.StatementListNode; public final class RepeatStatementNodeImpl extends DelphiNodeImpl implements RepeatStatementNode { public RepeatStatementNodeImpl(Token token) { super(token); } + @Override + public ExpressionNode getGuardExpression() { + return (ExpressionNode) getChild(2); + } + + @Override + public StatementListNode getStatementList() { + return (StatementListNode) getChild(0); + } + @Override public T accept(DelphiParserVisitor visitor, T data) { return visitor.visit(this, data); diff --git a/delphi-frontend/src/main/java/au/com/integradev/delphi/antlr/ast/node/RoutineImplementationNodeImpl.java b/delphi-frontend/src/main/java/au/com/integradev/delphi/antlr/ast/node/RoutineImplementationNodeImpl.java index f10cfe1fb..6a0d92dda 100644 --- a/delphi-frontend/src/main/java/au/com/integradev/delphi/antlr/ast/node/RoutineImplementationNodeImpl.java +++ b/delphi-frontend/src/main/java/au/com/integradev/delphi/antlr/ast/node/RoutineImplementationNodeImpl.java @@ -19,6 +19,10 @@ package au.com.integradev.delphi.antlr.ast.node; import au.com.integradev.delphi.antlr.ast.visitors.DelphiParserVisitor; +import au.com.integradev.delphi.cfg.ControlFlowGraphFactory; +import au.com.integradev.delphi.cfg.api.ControlFlowGraph; +import com.google.common.base.Supplier; +import com.google.common.base.Suppliers; import java.util.function.Function; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -36,6 +40,19 @@ public final class RoutineImplementationNodeImpl extends RoutineNodeImpl implements RoutineImplementationNode { private TypeNameDeclaration typeDeclaration; + private final Supplier cfgSupplier = + Suppliers.memoize( + () -> { + RoutineBodyNode routineBody = getRoutineBody(); + if (routineBody == null) { + return null; + } + CompoundStatementNode block = routineBody.getStatementBlock(); + if (block == null) { + return null; + } + return ControlFlowGraphFactory.create(block); + }); public RoutineImplementationNodeImpl(Token token) { super(token); @@ -127,6 +144,11 @@ public NameReferenceNode getNameReferenceNode() { return getRoutineNameNode().getNameReferenceNode(); } + @Nullable + public ControlFlowGraph getControlFlowGraph() { + return cfgSupplier.get(); + } + @Override public VisibilityType createVisibility() { return VisibilityType.PUBLIC; diff --git a/delphi-frontend/src/main/java/au/com/integradev/delphi/cfg/ControlFlowGraphBuilder.java b/delphi-frontend/src/main/java/au/com/integradev/delphi/cfg/ControlFlowGraphBuilder.java new file mode 100644 index 000000000..3af972454 --- /dev/null +++ b/delphi-frontend/src/main/java/au/com/integradev/delphi/cfg/ControlFlowGraphBuilder.java @@ -0,0 +1,251 @@ +/* + * Sonar Delphi Plugin + * Copyright (C) 2025 Integrated Application Development + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package au.com.integradev.delphi.cfg; + +import au.com.integradev.delphi.cfg.api.Block; +import au.com.integradev.delphi.cfg.api.ControlFlowGraph; +import au.com.integradev.delphi.cfg.block.BlockImpl; +import au.com.integradev.delphi.cfg.block.ProtoBlock; +import au.com.integradev.delphi.cfg.block.ProtoBlockFactory; +import com.google.common.collect.Lists; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Deque; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.sonar.plugins.communitydelphi.api.ast.DelphiNode; +import org.sonar.plugins.communitydelphi.api.ast.GotoStatementNode; +import org.sonar.plugins.communitydelphi.api.ast.LabelStatementNode; +import org.sonar.plugins.communitydelphi.api.ast.NameReferenceNode; +import org.sonar.plugins.communitydelphi.api.type.Type; + +public class ControlFlowGraphBuilder { + private final List blocks = new ArrayList<>(); + private final Deque exitBlocks = new ArrayDeque<>(); + private final Deque breakTargets = new ArrayDeque<>(); + private final Deque continueTargets = new ArrayDeque<>(); + private final Map labelTargets = new HashMap<>(); + private final Map> unresolvedLabels = new HashMap<>(); + private final Deque tryContexts = new ArrayDeque<>(); + + private ProtoBlock currentBlock; + + public ControlFlowGraphBuilder() { + ProtoBlock exitBlock = ProtoBlockFactory.exitBlock(); + addBlock(exitBlock); + exitBlocks.add(exitBlock); + addBlockBefore(exitBlock); + } + + public ControlFlowGraph build() { + Map map = new LinkedHashMap<>(); + for (ProtoBlock block : blocks) { + map.put(block, block.createBlock()); + } + for (ProtoBlock block : blocks) { + block.updateBlockData(map); + } + + ControlFlowGraphImpl cfg = + new ControlFlowGraphImpl( + map.get(currentBlock), map.get(exitBlocks.peek()), new ArrayList<>(map.values())); + cfg.prune(); + + populatePredecessors(cfg); + populateIds(cfg); + + return cfg; + } + + private static void populatePredecessors(ControlFlowGraph cfg) { + for (Block block : cfg.getBlocks()) { + for (Block successor : block.getSuccessors()) { + ((BlockImpl) successor).addPredecessor(block); + } + } + } + + private static void populateIds(ControlFlowGraph cfg) { + List blocks = Lists.reverse(cfg.getBlocks()); + for (int blockId = 0; blockId < blocks.size(); blockId++) { + ((BlockImpl) blocks.get(blockId)).setId(blockId); + } + } + + public ProtoBlock getExitBlock() { + return exitBlocks.peek(); + } + + public ProtoBlock getBreakTarget() { + return breakTargets.peek(); + } + + public ProtoBlock getContinueTarget() { + return continueTargets.peek(); + } + + public void pushLoopContext(ProtoBlock continueTarget, ProtoBlock breakTarget) { + continueTargets.push(continueTarget); + breakTargets.push(breakTarget); + } + + public void popLoopContext() { + breakTargets.pop(); + continueTargets.pop(); + } + + public void pushExitBlock(ProtoBlock target) { + exitBlocks.push(target); + } + + public void popExitBlock() { + exitBlocks.pop(); + } + + private static class UnresolvedLabel { + ProtoBlock nextBlock; + ProtoBlock block; + DelphiNode node; + } + + public void addLabel(LabelStatementNode labelNode) { + NameReferenceNode labelName = labelNode.getNameReference(); + String label = labelName.getImage(); + + labelTargets.put(label, currentBlock); + if (!unresolvedLabels.containsKey(label)) { + return; + } + + // When "resolving" label, all the previously unresolved targets must be updated + for (UnresolvedLabel unresolvedLabel : unresolvedLabels.get(label)) { + unresolvedLabel.block.update( + ProtoBlockFactory.jump(unresolvedLabel.node, currentBlock, unresolvedLabel.nextBlock)); + } + } + + public void addGoto(GotoStatementNode gotoNode) { + NameReferenceNode labelNode = gotoNode.getNameReference(); + String label = labelNode.getImage(); + if (labelTargets.containsKey(label)) { + addBlock(ProtoBlockFactory.jump(gotoNode, labelTargets.get(label), currentBlock)); + return; + } + + // When labels are used before they are processed they become `unresolved` + addBlockBeforeCurrent(); + unresolvedLabels.putIfAbsent(label, new ArrayList<>()); + UnresolvedLabel unresolvedLabel = new UnresolvedLabel(); + unresolvedLabel.nextBlock = currentBlock; + unresolvedLabel.block = addBlockBeforeCurrent(); + unresolvedLabel.node = gotoNode; + unresolvedLabels.get(label).add(unresolvedLabel); + + addElement(labelNode); + } + + private static class TryContext { + LinkedHashMap catches = new LinkedHashMap<>(); + ProtoBlock elseBlock; + } + + public void pushTryFinallyContext() { + tryContexts.push(new TryContext()); + } + + public void pushTryExceptContext(List> catches, ProtoBlock elseBlock) { + TryContext tryContext = new TryContext(); + tryContext.catches = new LinkedHashMap<>(); + catches.forEach(entry -> tryContext.catches.put(entry.getKey(), entry.getValue())); + tryContext.elseBlock = elseBlock; + tryContexts.push(tryContext); + } + + public boolean inTryContext() { + return !tryContexts.isEmpty(); + } + + public ProtoBlock getCatchTarget(Type exceptionType) { + if (tryContexts.isEmpty()) { + return getExitBlock(); + } + TryContext tryContext = tryContexts.peek(); + return tryContext.catches.keySet().stream() + .filter(catchType -> isCompatibleType(exceptionType, catchType)) + .findFirst() + .map(tryContext.catches::get) + .or(() -> Optional.ofNullable(tryContext.elseBlock)) + .orElse(getExitBlock()); + } + + private static boolean isCompatibleType(Type exceptionType, Type catchType) { + return exceptionType.is(catchType) || exceptionType.isDescendantOf(catchType); + } + + public Set getAllCatchTargets() { + if (tryContexts.isEmpty()) { + return Collections.emptySet(); + } + TryContext context = tryContexts.peek(); + Stream elseOrExit = + Stream.of(Optional.ofNullable(context.elseBlock).orElse(getExitBlock())); + return Stream.concat(context.catches.values().stream(), elseOrExit) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + } + + public void popTryContext() { + tryContexts.pop(); + } + + public ProtoBlock getCurrentBlock() { + return currentBlock; + } + + public void setCurrentBlock(ProtoBlock currentBlock) { + this.currentBlock = currentBlock; + } + + public void addElement(DelphiNode element) { + currentBlock.addElement(element); + } + + public ProtoBlock addBlockBeforeCurrent() { + addBlockBefore(currentBlock); + return currentBlock; + } + + public void addBlockBefore(ProtoBlock successor) { + addBlock(ProtoBlockFactory.linear(successor)); + } + + public void addBlock(ProtoBlock block) { + blocks.add(block); + currentBlock = block; + } +} diff --git a/delphi-frontend/src/main/java/au/com/integradev/delphi/cfg/ControlFlowGraphDebug.java b/delphi-frontend/src/main/java/au/com/integradev/delphi/cfg/ControlFlowGraphDebug.java new file mode 100644 index 000000000..91f8e8892 --- /dev/null +++ b/delphi-frontend/src/main/java/au/com/integradev/delphi/cfg/ControlFlowGraphDebug.java @@ -0,0 +1,95 @@ +/* + * Sonar Delphi Plugin + * Copyright (C) 2025 Integrated Application Development + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package au.com.integradev.delphi.cfg; + +import au.com.integradev.delphi.cfg.api.Block; +import au.com.integradev.delphi.cfg.api.ControlFlowGraph; +import au.com.integradev.delphi.cfg.api.Terminated; +import au.com.integradev.delphi.cfg.block.BlockImpl; +import java.util.Optional; +import java.util.stream.IntStream; +import org.sonar.plugins.communitydelphi.api.ast.BinaryExpressionNode; +import org.sonar.plugins.communitydelphi.api.ast.DelphiNode; + +public final class ControlFlowGraphDebug { + private static final int MAX_NODE_TYPE_NAME = 30; + + private ControlFlowGraphDebug() { + // Utility class + } + + public static String toString(ControlFlowGraph cfg) { + StringBuilder buffer = new StringBuilder(); + buffer.append("Starts at "); + buffer.append(getBlockString(cfg.getEntryBlock())); + buffer.append('\n'); + buffer.append('\n'); + for (Block block : cfg.getBlocks()) { + buffer.append(toString(block)); + } + return buffer.toString(); + } + + public static String toString(Block block) { + StringBuilder buffer = new StringBuilder(); + buffer.append(getBlockString(block)); + + IntStream.range(0, block.getElements().size()) + .forEach(index -> appendElement(buffer, index, block.getElements().get(index))); + + getAs(block, Terminated.class) + .ifPresent( + successors -> { + buffer.append("\nT:\t"); + appendKind(buffer, successors.getTerminator()); + buffer.append(successors.getTerminator().getImage()); + }); + + buffer.append(getAs(block, BlockImpl.class).orElseThrow().getDescription()); + buffer.append("\n\n"); + return buffer.toString(); + } + + private static void appendKind(StringBuilder buffer, DelphiNode node) { + String name = node.getClass().getSimpleName(); + if (node instanceof BinaryExpressionNode) { + name += " " + ((BinaryExpressionNode) node).getOperator(); + } + buffer.append(String.format("%-" + MAX_NODE_TYPE_NAME + "s\t", name)); + } + + private static void appendElement(StringBuilder buffer, int index, DelphiNode node) { + buffer.append('\n'); + buffer.append(index); + buffer.append(":\t"); + appendKind(buffer, node); + buffer.append(node.getImage()); + } + + private static String getBlockString(Block block) { + return "B" + ((BlockImpl) block).getId(); + } + + private static Optional getAs(Block block, Class clazz) { + if (clazz.isInstance(block)) { + return Optional.of(clazz.cast(block)); + } + return Optional.empty(); + } +} diff --git a/delphi-frontend/src/main/java/au/com/integradev/delphi/cfg/ControlFlowGraphFactory.java b/delphi-frontend/src/main/java/au/com/integradev/delphi/cfg/ControlFlowGraphFactory.java new file mode 100644 index 000000000..669cfcbb6 --- /dev/null +++ b/delphi-frontend/src/main/java/au/com/integradev/delphi/cfg/ControlFlowGraphFactory.java @@ -0,0 +1,57 @@ +/* + * Sonar Delphi Plugin + * Copyright (C) 2025 Integrated Application Development + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package au.com.integradev.delphi.cfg; + +import au.com.integradev.delphi.cfg.api.ControlFlowGraph; +import com.google.common.collect.Lists; +import java.util.List; +import org.sonar.plugins.communitydelphi.api.ast.AnonymousMethodNode; +import org.sonar.plugins.communitydelphi.api.ast.CompoundStatementNode; +import org.sonar.plugins.communitydelphi.api.ast.RoutineImplementationNode; +import org.sonar.plugins.communitydelphi.api.ast.StatementListNode; +import org.sonar.plugins.communitydelphi.api.ast.StatementNode; + +public final class ControlFlowGraphFactory { + private ControlFlowGraphFactory() { + // Utility class + } + + public static ControlFlowGraph create(RoutineImplementationNode routine) { + return create(routine.getRoutineBody().getStatementBlock()); + } + + public static ControlFlowGraph create(AnonymousMethodNode anonymousMethod) { + return create(anonymousMethod.getFirstDescendantOfType(CompoundStatementNode.class)); + } + + public static ControlFlowGraph create(CompoundStatementNode initialNode) { + return create(initialNode.getStatementList()); + } + + public static ControlFlowGraph create(StatementListNode statements) { + return create(statements.getStatements()); + } + + public static ControlFlowGraph create(List statements) { + ControlFlowGraphBuilder builder = new ControlFlowGraphBuilder(); + ControlFlowGraphVisitor visitor = new ControlFlowGraphVisitor(); + Lists.reverse(statements).forEach(statement -> statement.accept(visitor, builder)); + return builder.build(); + } +} diff --git a/delphi-frontend/src/main/java/au/com/integradev/delphi/cfg/ControlFlowGraphImpl.java b/delphi-frontend/src/main/java/au/com/integradev/delphi/cfg/ControlFlowGraphImpl.java new file mode 100644 index 000000000..f5db511cd --- /dev/null +++ b/delphi-frontend/src/main/java/au/com/integradev/delphi/cfg/ControlFlowGraphImpl.java @@ -0,0 +1,118 @@ +/* + * Sonar Delphi Plugin + * Copyright (C) 2025 Integrated Application Development + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package au.com.integradev.delphi.cfg; + +import au.com.integradev.delphi.cfg.api.Block; +import au.com.integradev.delphi.cfg.api.ControlFlowGraph; +import au.com.integradev.delphi.cfg.api.Terminated; +import au.com.integradev.delphi.cfg.api.UnconditionalJump; +import au.com.integradev.delphi.cfg.block.BlockImpl; +import com.google.common.collect.Lists; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +public class ControlFlowGraphImpl implements ControlFlowGraph { + private Block entry; + private final Block exit; + private final List blocks; + + public ControlFlowGraphImpl(Block entry, Block exit, List blocks) { + this.entry = entry; + this.exit = exit; + this.blocks = blocks; + } + + @Override + public Block getEntryBlock() { + return entry; + } + + @Override + public Block getExitBlock() { + return exit; + } + + @Override + public List getBlocks() { + return Collections.unmodifiableList(Lists.reverse(blocks)); + } + + /** Removes redundant blocks from the graph and updates their neighbouring blocks. */ + public void prune() { + Set inactiveBlocks = new HashSet<>(); + + do { + inactiveBlocks.clear(); + blocks.stream().skip(1).filter(this::isInactive).forEach(inactiveBlocks::add); + + if (inactiveBlocks.isEmpty()) { + break; + } + + removeBlocks(inactiveBlocks); + if (inactiveBlocks.contains(this.entry)) { + this.entry = this.entry.getSuccessors().iterator().next(); + } + + blocks.forEach(inactiveBlocks::remove); + } while (!inactiveBlocks.isEmpty()); + } + + private boolean isInactive(Block block) { + if (block == this.entry && block.getSuccessors().size() > 1) { + return false; + } + + return !(block instanceof Terminated) + && block.getElements().isEmpty() + && block.getSuccessors().size() == 1; + } + + private void removeBlocks(Set inactiveBlocks) { + for (Block inactiveBlock : inactiveBlocks) { + Block successor = inactiveBlock.getSuccessors().iterator().next(); + for (Block block : blocks) { + replaceSuccessorWith(block, inactiveBlock, successor); + } + } + blocks.removeAll(inactiveBlocks); + } + + private static void replaceSuccessorWith(Block block, Block inactiveBlock, Block successor) { + if (!block.getSuccessors().contains(inactiveBlock) + && getAs(block, UnconditionalJump.class) + .map(jump -> jump.getSuccessorIfRemoved() != inactiveBlock) + .orElse(true)) { + return; + } + + BlockImpl blockImpl = getAs(block, BlockImpl.class).orElseThrow(); + blockImpl.replaceInactiveSuccessor(inactiveBlock, successor); + } + + private static Optional getAs(T subject, Class type) { + if (type.isInstance(subject)) { + return Optional.of(type.cast(subject)); + } + return Optional.empty(); + } +} diff --git a/delphi-frontend/src/main/java/au/com/integradev/delphi/cfg/ControlFlowGraphVisitor.java b/delphi-frontend/src/main/java/au/com/integradev/delphi/cfg/ControlFlowGraphVisitor.java new file mode 100644 index 000000000..3b09b225d --- /dev/null +++ b/delphi-frontend/src/main/java/au/com/integradev/delphi/cfg/ControlFlowGraphVisitor.java @@ -0,0 +1,734 @@ +/* + * Sonar Delphi Plugin + * Copyright (C) 2025 Integrated Application Development + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package au.com.integradev.delphi.cfg; + +import au.com.integradev.delphi.antlr.ast.visitors.DelphiParserVisitor; +import au.com.integradev.delphi.cfg.block.ProtoBlock; +import au.com.integradev.delphi.cfg.block.ProtoBlockFactory; +import java.util.AbstractMap.SimpleEntry; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map.Entry; +import java.util.Set; +import java.util.stream.Collectors; +import org.sonar.plugins.communitydelphi.api.ast.AnonymousMethodNode; +import org.sonar.plugins.communitydelphi.api.ast.ArgumentListNode; +import org.sonar.plugins.communitydelphi.api.ast.ArgumentNode; +import org.sonar.plugins.communitydelphi.api.ast.ArrayConstructorNode; +import org.sonar.plugins.communitydelphi.api.ast.AsmStatementNode; +import org.sonar.plugins.communitydelphi.api.ast.AssignmentStatementNode; +import org.sonar.plugins.communitydelphi.api.ast.BinaryExpressionNode; +import org.sonar.plugins.communitydelphi.api.ast.CaseItemStatementNode; +import org.sonar.plugins.communitydelphi.api.ast.CaseStatementNode; +import org.sonar.plugins.communitydelphi.api.ast.ConstStatementNode; +import org.sonar.plugins.communitydelphi.api.ast.DelphiNode; +import org.sonar.plugins.communitydelphi.api.ast.ExceptBlockNode; +import org.sonar.plugins.communitydelphi.api.ast.ExceptItemNode; +import org.sonar.plugins.communitydelphi.api.ast.ExpressionNode; +import org.sonar.plugins.communitydelphi.api.ast.ExpressionStatementNode; +import org.sonar.plugins.communitydelphi.api.ast.FinallyBlockNode; +import org.sonar.plugins.communitydelphi.api.ast.ForInStatementNode; +import org.sonar.plugins.communitydelphi.api.ast.ForLoopVarDeclarationNode; +import org.sonar.plugins.communitydelphi.api.ast.ForLoopVarReferenceNode; +import org.sonar.plugins.communitydelphi.api.ast.ForStatementNode; +import org.sonar.plugins.communitydelphi.api.ast.ForToStatementNode; +import org.sonar.plugins.communitydelphi.api.ast.GotoStatementNode; +import org.sonar.plugins.communitydelphi.api.ast.IfStatementNode; +import org.sonar.plugins.communitydelphi.api.ast.IntegerLiteralNode; +import org.sonar.plugins.communitydelphi.api.ast.LabelStatementNode; +import org.sonar.plugins.communitydelphi.api.ast.NameDeclarationListNode; +import org.sonar.plugins.communitydelphi.api.ast.NameReferenceNode; +import org.sonar.plugins.communitydelphi.api.ast.NilLiteralNode; +import org.sonar.plugins.communitydelphi.api.ast.RaiseStatementNode; +import org.sonar.plugins.communitydelphi.api.ast.RangeExpressionNode; +import org.sonar.plugins.communitydelphi.api.ast.RealLiteralNode; +import org.sonar.plugins.communitydelphi.api.ast.RepeatStatementNode; +import org.sonar.plugins.communitydelphi.api.ast.SimpleNameDeclarationNode; +import org.sonar.plugins.communitydelphi.api.ast.StatementListNode; +import org.sonar.plugins.communitydelphi.api.ast.StatementNode; +import org.sonar.plugins.communitydelphi.api.ast.TextLiteralNode; +import org.sonar.plugins.communitydelphi.api.ast.TryStatementNode; +import org.sonar.plugins.communitydelphi.api.ast.UnaryExpressionNode; +import org.sonar.plugins.communitydelphi.api.ast.VarStatementNode; +import org.sonar.plugins.communitydelphi.api.ast.WhileStatementNode; +import org.sonar.plugins.communitydelphi.api.ast.WithStatementNode; +import org.sonar.plugins.communitydelphi.api.ast.utils.ExpressionNodeUtils; +import org.sonar.plugins.communitydelphi.api.operator.BinaryOperator; +import org.sonar.plugins.communitydelphi.api.symbol.declaration.RoutineNameDeclaration; +import org.sonar.plugins.communitydelphi.api.type.Type; +import org.sonarsource.analyzer.commons.collections.ListUtils; + +/** + * This visitor populates the {@link ControlFlowGraphBuilder} to construct a control flow graph. + * Generally, the statements and elements are traversed backward simplify the construction of a + * directed graph. `Block` objects are typically are ordered in way they are evaluated. + */ +class ControlFlowGraphVisitor implements DelphiParserVisitor { + + // Literals / Block elements + // These nodes get added to the current block + + @Override + public ControlFlowGraphBuilder visit(IntegerLiteralNode node, ControlFlowGraphBuilder builder) { + builder.addElement(node); + return builder; + } + + @Override + public ControlFlowGraphBuilder visit(RealLiteralNode node, ControlFlowGraphBuilder builder) { + builder.addElement(node); + return builder; + } + + @Override + public ControlFlowGraphBuilder visit(NilLiteralNode node, ControlFlowGraphBuilder builder) { + builder.addElement(node); + return builder; + } + + @Override + public ControlFlowGraphBuilder visit(TextLiteralNode node, ControlFlowGraphBuilder builder) { + builder.addElement(node); + return builder; + } + + @Override + public ControlFlowGraphBuilder visit( + SimpleNameDeclarationNode node, ControlFlowGraphBuilder builder) { + builder.addElement(node); + return builder; + } + + @Override + public ControlFlowGraphBuilder visit(RangeExpressionNode node, ControlFlowGraphBuilder builder) { + build(node.getHighExpression(), builder); + return build(node.getLowExpression(), builder); + } + + @Override + public ControlFlowGraphBuilder visit(ArrayConstructorNode node, ControlFlowGraphBuilder builder) { + return build(node.getElements(), builder); + } + + @Override + public ControlFlowGraphBuilder visit( + ForLoopVarDeclarationNode node, ControlFlowGraphBuilder builder) { + return build(node.getNameDeclarationNode(), builder); + } + + @Override + public ControlFlowGraphBuilder visit( + ForLoopVarReferenceNode node, ControlFlowGraphBuilder builder) { + return build(node.getNameReference(), builder); + } + + @Override + public ControlFlowGraphBuilder visit(ArgumentNode node, ControlFlowGraphBuilder builder) { + return build(node.getExpression(), builder); + } + + /* + * NameReferenceNode has overloaded meanings in Delphi. The control flow intrinsics are + * handled individually. + */ + @Override + public ControlFlowGraphBuilder visit(NameReferenceNode node, ControlFlowGraphBuilder builder) { + if (!(node.getLastName().getNameDeclaration() instanceof RoutineNameDeclaration)) { + builder.addElement(node); + return builder; + } + String routineName = + ((RoutineNameDeclaration) node.getLastName().getNameDeclaration()).fullyQualifiedName(); + switch (routineName) { + case "System.Exit": + builder.addBlock( + ProtoBlockFactory.jump(node, builder.getExitBlock(), builder.getCurrentBlock())); + return builder; + case "System.Break": + return buildControlFlowStatement(node, builder.getBreakTarget(), builder); + case "System.Halt": + builder.addBlock(ProtoBlockFactory.halt(node)); + return builder; + case "System.Continue": + return buildControlFlowStatement(node, builder.getContinueTarget(), builder); + default: + handleExceptionalPaths(builder); + builder.addElement(node); + break; + } + + return builder; + } + + private static ControlFlowGraphBuilder buildControlFlowStatement( + DelphiNode node, ProtoBlock target, ControlFlowGraphBuilder builder) { + if (target == null) { + throw new IllegalStateException( + String.format("'%s' statement not in loop statement", node.getImage())); + } + builder.addBlock(ProtoBlockFactory.jump(node, target, builder.getCurrentBlock())); + return builder; + } + + private static void handleExceptionalPaths(ControlFlowGraphBuilder builder) { + if (!builder.inTryContext()) { + // Only consider routines as potentially exceptional if in a `try` context. + return; + } + + Set exceptions = builder.getAllCatchTargets(); + builder.addBlock(ProtoBlockFactory.withExceptions(builder.getCurrentBlock(), exceptions)); + } + + // Overridden to ensure NodeDeclarations are added in the correct order + @Override + public ControlFlowGraphBuilder visit( + NameDeclarationListNode node, ControlFlowGraphBuilder builder) { + return build(node.getDeclarations(), builder); + } + + // ExpressionNode covers `inherited` expressions + @Override + public ControlFlowGraphBuilder visit(ExpressionNode node, ControlFlowGraphBuilder builder) { + if (!ExpressionNodeUtils.isInherited(node)) { + return DelphiParserVisitor.super.visit(node, builder); + } + + // Arguments + ArgumentListNode arguments = node.getFirstChildOfType(ArgumentListNode.class); + if (arguments != null) { + build(arguments.getArgumentNodes(), builder); + } + + // Possible name + build(node.getFirstChildOfType(NameReferenceNode.class), builder); + + // `inherited` + builder.addElement(node.skipParentheses().getChild(0)); + return builder; + } + + // Statements + + @Override + public ControlFlowGraphBuilder visit(StatementListNode node, ControlFlowGraphBuilder builder) { + return build(node.getStatements(), builder); + } + + /* + * From the condition there are two successors, a true branch and a false branch. Both branches + * will have successors of the block that comes after. + * + * `if Condition then ThenBlock else ElseBlock;` maps to + * + * ┌─> true ──> `ThenBlock` ─┐ + * `Condition` ─┤ ├─> after + * └─> false ─> `ElseBlock` ─┘ + */ + @Override + public ControlFlowGraphBuilder visit(IfStatementNode node, ControlFlowGraphBuilder builder) { + ProtoBlock after = builder.getCurrentBlock(); + + // process `else` + builder.addBlockBefore(after); + build(node.getElseStatement(), builder); + ProtoBlock elseBlock = builder.getCurrentBlock(); + + // process `then` + builder.addBlockBefore(after); + build(node.getThenStatement(), builder); + ProtoBlock thenBlock = builder.getCurrentBlock(); + + // process condition + builder.addBlock(ProtoBlockFactory.branch(node, thenBlock, elseBlock)); + return buildCondition(builder, node.getGuardExpression(), thenBlock, elseBlock); + } + + private ControlFlowGraphBuilder buildCondition( + ControlFlowGraphBuilder builder, + ExpressionNode node, + ProtoBlock trueBlock, + ProtoBlock falseBlock) { + node = node.skipParentheses(); + if (!(node instanceof BinaryExpressionNode)) { + return build(node, builder); + } + BinaryExpressionNode binaryExpression = (BinaryExpressionNode) node; + BinaryOperator operator = binaryExpression.getOperator(); + if (operator == BinaryOperator.OR) { + return buildConditionOr(builder, binaryExpression, trueBlock, falseBlock); + } else if (operator == BinaryOperator.AND) { + return buildConditionAnd(builder, binaryExpression, trueBlock, falseBlock); + } + return build(node, builder); + } + + private ControlFlowGraphBuilder buildConditionAnd( + ControlFlowGraphBuilder builder, + BinaryExpressionNode node, + ProtoBlock trueBlock, + ProtoBlock falseBlock) { + // RHS + buildCondition(builder, node.getRight(), trueBlock, falseBlock); + ProtoBlock newTrueBlock = builder.getCurrentBlock(); + // LHS + builder.addBlock(ProtoBlockFactory.branch(node, newTrueBlock, falseBlock)); + return buildCondition(builder, node.getLeft(), newTrueBlock, falseBlock); + } + + private ControlFlowGraphBuilder buildConditionOr( + ControlFlowGraphBuilder builder, + BinaryExpressionNode node, + ProtoBlock trueBlock, + ProtoBlock falseBlock) { + // RHS + buildCondition(builder, node.getRight(), trueBlock, falseBlock); + ProtoBlock newFalseBlock = builder.getCurrentBlock(); + // LHS + builder.addBlock(ProtoBlockFactory.branch(node, trueBlock, newFalseBlock)); + return buildCondition(builder, node.getLeft(), trueBlock, newFalseBlock); + } + + @Override + public ControlFlowGraphBuilder visit(VarStatementNode node, ControlFlowGraphBuilder builder) { + build(node.getNameDeclarationList(), builder); + return build(node.getExpression(), builder); + } + + @Override + public ControlFlowGraphBuilder visit(ConstStatementNode node, ControlFlowGraphBuilder builder) { + build(node.getNameDeclarationNode(), builder); + return build(node.getExpression(), builder); + } + + /* + * The case's selector expression has a successors of each case, and the optional else block. + * + * case Selector of + * A: Body1; + * B: Body2; + * else + * Body3; + * end; + * + * maps to + * ┌─> `A` => `Body1` ─┐ + * `Selector` ─┼─> `B` => `Body2` ─┼─> after + * └─> `else` => `Body3` ─┘ + */ + @Override + public ControlFlowGraphBuilder visit(CaseStatementNode node, ControlFlowGraphBuilder builder) { + ProtoBlock after = builder.getCurrentBlock(); + + // Selector + ProtoBlock caseBlock = builder.addBlockBeforeCurrent(); + + List caseLabels = + node.getCaseItems().stream() + .flatMap(caseNode -> caseNode.getExpressions().stream()) + .collect(Collectors.toList()); + build(caseLabels, builder); + build(node.getSelectorExpression(), builder); + ProtoBlock conditionBlock = builder.getCurrentBlock(); + + Set caseSuccessors = new HashSet<>(); + + // If none of the case arms match, it will either skip the block, or + // fallthrough to an `else` block + ProtoBlock fallthrough = after; + // Else + if (node.getElseBlockNode() != null) { + builder.addBlockBefore(after); + build(node.getElseBlockNode().getStatementList(), builder); + fallthrough = builder.getCurrentBlock(); + } + + // Cases + for (StatementNode statement : + ListUtils.reverse( + node.getCaseItems().stream() + .map(CaseItemStatementNode::getStatement) + .collect(Collectors.toList()))) { + builder.addBlockBefore(after); + build(statement, builder); + caseSuccessors.add(builder.getCurrentBlock()); + } + + caseBlock.update(ProtoBlockFactory.cases(node, caseSuccessors, fallthrough)); + builder.setCurrentBlock(conditionBlock); + return builder; + } + + /* + * Repeat statements flow through the body first, then consult the condition as to where to go + * next. + * + * repeat + * Body; + * until Condition; + * + * maps to + * ┌─> true ────┐ + * `Body` ─> `Condition` ─┴─> false ─┐ └─> after + * ^───────────────────────────────┘ + */ + @Override + public ControlFlowGraphBuilder visit(RepeatStatementNode node, ControlFlowGraphBuilder builder) { + ProtoBlock after = builder.getCurrentBlock(); + // Create a placeholder for the body's starting block + ProtoBlock body = builder.addBlockBeforeCurrent(); + + // Condition + builder.addBlock(ProtoBlockFactory.branch(node, after, body)); + buildCondition(builder, node.getGuardExpression(), after, body); + + // Body + builder.pushLoopContext(builder.getCurrentBlock(), after); + builder.addBlockBeforeCurrent(); + build(node.getStatementList(), builder); + builder.popLoopContext(); + + body.update(ProtoBlockFactory.linear(builder.getCurrentBlock())); + builder.addBlockBeforeCurrent(); + return builder; + } + + /* + * While loops flow through the condition first, then based on its value either enter the loop or + * continue on. + * + * `while Condition do Body;` maps to + * + * ┌─> false ─────────────┐ + * `Condition` ─┴─> true ──> `Body` ─┐ └─> after + * ^─────────────────────────────┘ + */ + @Override + public ControlFlowGraphBuilder visit(WhileStatementNode node, ControlFlowGraphBuilder builder) { + ProtoBlock after = builder.getCurrentBlock(); + // Create a placeholder for the condition's starting block + ProtoBlock condition = builder.addBlockBeforeCurrent(); + + // Body + builder.addBlockBefore(condition); + builder.pushLoopContext(condition, after); + build(node.getStatement(), builder); + builder.popLoopContext(); + ProtoBlock body = builder.getCurrentBlock(); + + // Condition + builder.setCurrentBlock(condition); + builder.addBlock(ProtoBlockFactory.branch(node, body, after)); + buildCondition(builder, node.getGuardExpression(), body, after); + + condition.update(ProtoBlockFactory.linear(builder.getCurrentBlock())); + builder.addBlockBeforeCurrent(); + return builder; + } + + /* + * For to/downto loops evaluate in order the low value expression, the high expression, the body, + * and the variable. The variable has branching behaviour based on whether there is a next element + * in the range. + * + * `for A := B to C do Body;` maps to + * + * `B` ─> `C` ─> `Body` ─> `A` ─┬─> false ─> after + * ^─── true <──┘ + */ + @Override + public ControlFlowGraphBuilder visit(ForToStatementNode node, ControlFlowGraphBuilder builder) { + return buildForLoop( + node, + List.of(node.getVariable(), node.getTargetExpression(), node.getInitializerExpression()), + builder); + } + + /* + * For in loops evaluate in order the enumerable, the body, and the variable. The variable has + * branching behaviour based on whether there is a next element in the enumerable. + * + * `for A in B do Body;` maps to + * + * `B` ─> `Body` ─> `A` ─┬─> false ─> after + * ^─── true <──┘ + */ + @Override + public ControlFlowGraphBuilder visit(ForInStatementNode node, ControlFlowGraphBuilder builder) { + return buildForLoop(node, List.of(node.getVariable(), node.getEnumerable()), builder); + } + + private ControlFlowGraphBuilder buildForLoop( + ForStatementNode node, List parts, ControlFlowGraphBuilder builder) { + ProtoBlock after = builder.getCurrentBlock(); + // Create a placeholder for the conditional block + ProtoBlock loopback = builder.addBlockBeforeCurrent(); + builder.addBlockBeforeCurrent(); + + builder.pushLoopContext(loopback, after); + build(node.getStatement(), builder); + builder.popLoopContext(); + + loopback.update(ProtoBlockFactory.branch(node, builder.getCurrentBlock(), after)); + + builder.setCurrentBlock(loopback); + parts.forEach( + part -> { + build(part, builder); + builder.addBlockBeforeCurrent(); + }); + return builder; + } + + @Override + public ControlFlowGraphBuilder visit(WithStatementNode node, ControlFlowGraphBuilder builder) { + builder.addBlockBeforeCurrent(); + build(node.getStatement(), builder); + builder.addBlockBeforeCurrent(); + build(node.getTargets(), builder); + builder.addBlockBeforeCurrent(); + return builder; + } + + /* + * Try statements add handling for exceptions. There is a direct path from the `finally` block to + * the local exit block. This path would be used when another control flow altering statement is + * used, such as `Exit`, `Break`, and exceptions. Within `try` statements, routine invocations + * gain a successor to the catches/finally block. + */ + @Override + public ControlFlowGraphBuilder visit(TryStatementNode node, ControlFlowGraphBuilder builder) { + if (node.getFinallyBlock() != null) { + return buildTryFinally(node, builder); + } else { + return buildTryExcept(node, builder); + } + } + + private ControlFlowGraphBuilder buildTryFinally( + TryStatementNode node, ControlFlowGraphBuilder builder) { + builder.addBlockBeforeCurrent(); + // Finally + FinallyBlockNode finallyNode = node.getFinallyBlock(); + builder.addBlock( + ProtoBlockFactory.finallyBlock(builder.getCurrentBlock(), builder.getExitBlock())); + build(finallyNode.getStatementList(), builder); + builder.pushLoopContext(builder.getCurrentBlock(), builder.getCurrentBlock()); + builder.pushExitBlock(builder.getCurrentBlock()); + + builder.addBlockBeforeCurrent(); + + // Body + builder.pushTryFinallyContext(); + build(node.getStatementList(), builder); + builder.popTryContext(); + + builder.addBlockBeforeCurrent(); + builder.addElement(node); + + builder.popExitBlock(); + return builder; + } + + private ControlFlowGraphBuilder buildTryExcept( + TryStatementNode node, ControlFlowGraphBuilder builder) { + ProtoBlock endBlock = builder.getCurrentBlock(); + ProtoBlock beforeEnd = builder.addBlockBeforeCurrent(); + + // Exception catches + List> catches = new ArrayList<>(); + ProtoBlock elseBlock = null; + + ExceptBlockNode exceptBlock = node.getExceptBlock(); + if (exceptBlock.isBareExcept()) { + builder.addBlockBefore(endBlock); + build(exceptBlock.getStatementList(), builder); + elseBlock = builder.getCurrentBlock(); + } else if (exceptBlock.getElseBlock() != null) { + builder.addBlockBefore(endBlock); + build(exceptBlock.getElseBlock().getStatementList(), builder); + elseBlock = builder.getCurrentBlock(); + } + if (exceptBlock.hasHandlers()) { + for (ExceptItemNode exceptItem : ListUtils.reverse(exceptBlock.getHandlers())) { + builder.addBlockBefore(endBlock); + build(exceptItem.getStatement(), builder); + build(exceptItem.getExceptionName(), builder); + catches.add( + 0, + new SimpleEntry<>(exceptItem.getExceptionType().getType(), builder.getCurrentBlock())); + } + } + + // Body + builder.setCurrentBlock(beforeEnd); + + builder.pushTryExceptContext(catches, elseBlock); + build(node.getStatementList(), builder); + builder.popTryContext(); + + builder.addBlockBeforeCurrent(); + builder.addElement(node); + + return builder; + } + + /* + * `raise` statements jump directly to the handling exception or exit block. Bare raise statements + * have successors of all exceptional targets. + */ + @Override + public ControlFlowGraphBuilder visit(RaiseStatementNode node, ControlFlowGraphBuilder builder) { + if (node.getRaiseExpression() == null) { + Set exceptions = builder.getAllCatchTargets(); + builder.addBlock(ProtoBlockFactory.withExceptions(builder.getCurrentBlock(), exceptions)); + builder.addElement(node); + return builder; + } + + Type raiseType = node.getRaiseExpression().getType(); + ProtoBlock jumpTarget = builder.getCatchTarget(raiseType); + builder.addBlock(ProtoBlockFactory.jump(node, jumpTarget, builder.getCurrentBlock())); + return build(node.getRaiseExpression(), builder); + } + + // Label statements create a new block as they allow for the control flow to jump to them. + @Override + public ControlFlowGraphBuilder visit(LabelStatementNode node, ControlFlowGraphBuilder builder) { + build(node.getStatement(), builder); + builder.addLabel(node); + builder.addBlockBeforeCurrent(); + return builder; + } + + // `goto` statements have a successor of the label they jump to. + @Override + public ControlFlowGraphBuilder visit(GotoStatementNode node, ControlFlowGraphBuilder builder) { + builder.addGoto(node); + return builder; + } + + @Override + public ControlFlowGraphBuilder visit( + AssignmentStatementNode node, ControlFlowGraphBuilder builder) { + build(node.getAssignee(), builder); + return build(node.getValue(), builder); + } + + @Override + public ControlFlowGraphBuilder visit( + ExpressionStatementNode node, ControlFlowGraphBuilder builder) { + return build(node.getExpression(), builder); + } + + // Expressions + + @Override + public ControlFlowGraphBuilder visit(UnaryExpressionNode node, ControlFlowGraphBuilder builder) { + builder.addElement(node); + return build(node.getOperand(), builder); + } + + @Override + public ControlFlowGraphBuilder visit(BinaryExpressionNode node, ControlFlowGraphBuilder builder) { + boolean isBooleanExpr = node.getType().isBoolean(); + if (isBooleanExpr && node.getOperator() == BinaryOperator.AND) { + return buildBooleanAnd(node, builder); + } else if (isBooleanExpr && node.getOperator() == BinaryOperator.OR) { + return buildBooleanOr(node, builder); + } + + builder.addElement(node); + build(node.getRight(), builder); + build(node.getLeft(), builder); + return builder; + } + + /* + * Boolean `and` expressions represent the short-circuiting behaviour. `A and B` is as follows: + * + * ┌─> true ─> `B` ─┐ + * `A` ─┤ ├─> after + * └─> false ───────┘ + */ + private ControlFlowGraphBuilder buildBooleanAnd( + BinaryExpressionNode node, ControlFlowGraphBuilder builder) { + ProtoBlock falseBlock = builder.getCurrentBlock(); + builder.addBlockBefore(falseBlock); + build(node.getRight(), builder); + return buildBooleanLHS(builder, node, builder.getCurrentBlock(), falseBlock); + } + + /* + * Boolean `or` expressions represent the short-circuiting behaviour. `A or B` is as follows: + * + * ┌─> true ─────────┐ + * `A` ─┤ ├─> after + * └─> false ─> `B` ─┘ + */ + private ControlFlowGraphBuilder buildBooleanOr( + BinaryExpressionNode node, ControlFlowGraphBuilder builder) { + ProtoBlock trueBlock = builder.getCurrentBlock(); + builder.addBlockBefore(trueBlock); + build(node.getRight(), builder); + return buildBooleanLHS(builder, node, trueBlock, builder.getCurrentBlock()); + } + + private ControlFlowGraphBuilder buildBooleanLHS( + ControlFlowGraphBuilder builder, + BinaryExpressionNode node, + ProtoBlock trueBlock, + ProtoBlock falseBlock) { + builder.addBlock(ProtoBlockFactory.branch(node, trueBlock, falseBlock)); + return build(node.getLeft(), builder); + } + + // Exclusions + + /* + * Anonymous methods have their own associated control flow graph. One that is separate to the + * current one being constructed. + */ + @Override + public ControlFlowGraphBuilder visit(AnonymousMethodNode node, ControlFlowGraphBuilder builder) { + return builder; + } + + // Assembly control flow graphs are not supported. + @Override + public ControlFlowGraphBuilder visit(AsmStatementNode node, ControlFlowGraphBuilder builder) { + return builder; + } + + // Utils + + private ControlFlowGraphBuilder build(DelphiNode node, ControlFlowGraphBuilder builder) { + if (node == null) { + return builder; + } + return node.accept(this, builder); + } + + private ControlFlowGraphBuilder build( + List nodes, ControlFlowGraphBuilder builder) { + ListUtils.reverse(nodes).forEach(node -> build(node, builder)); + return builder; + } +} diff --git a/delphi-frontend/src/main/java/au/com/integradev/delphi/cfg/api/Block.java b/delphi-frontend/src/main/java/au/com/integradev/delphi/cfg/api/Block.java new file mode 100644 index 000000000..70f39d2d9 --- /dev/null +++ b/delphi-frontend/src/main/java/au/com/integradev/delphi/cfg/api/Block.java @@ -0,0 +1,49 @@ +/* + * Sonar Delphi Plugin + * Copyright (C) 2025 Integrated Application Development + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package au.com.integradev.delphi.cfg.api; + +import java.util.List; +import java.util.Set; +import org.sonar.plugins.communitydelphi.api.ast.DelphiNode; + +/** The unit of data within a {@link ControlFlowGraph} */ +public interface Block { + /** + * Successor blocks of the current block in the {@link ControlFlowGraph}, i.e., the blocks that + * could be next in the control flow + * + * @return the set of successor blocks + */ + Set getSuccessors(); + + /** + * Predecessors of the block in the {@link ControlFlowGraph}, i.e., the blocks which could succeed + * to this block + * + * @return the set of predecessor blocks + */ + Set getPredecessors(); + + /** + * Elements of the block, e.g., variable names and expressions + * + * @return the list of elements + */ + List getElements(); +} diff --git a/delphi-frontend/src/main/java/au/com/integradev/delphi/cfg/api/Branch.java b/delphi-frontend/src/main/java/au/com/integradev/delphi/cfg/api/Branch.java new file mode 100644 index 000000000..93f26eb25 --- /dev/null +++ b/delphi-frontend/src/main/java/au/com/integradev/delphi/cfg/api/Branch.java @@ -0,0 +1,43 @@ +/* + * Sonar Delphi Plugin + * Copyright (C) 2025 Integrated Application Development + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package au.com.integradev.delphi.cfg.api; + +import java.util.Set; + +/** A block where the control flow is dictated by a boolean condition, e.g., {@code if} */ +public interface Branch extends Block, Terminated { + /** + * Next block in the {@link ControlFlowGraph} when the condition is {@code true} + * + * @return the successor block if the condition is {@code true} + */ + Block getTrueBlock(); + + /** + * Next block in the {@link ControlFlowGraph} when the condition is {@code false} + * + * @return the successor block if the condition is {@code false} + */ + Block getFalseBlock(); + + @Override + default Set getSuccessors() { + return Set.of(getTrueBlock(), getFalseBlock()); + } +} diff --git a/delphi-frontend/src/main/java/au/com/integradev/delphi/cfg/api/Cases.java b/delphi-frontend/src/main/java/au/com/integradev/delphi/cfg/api/Cases.java new file mode 100644 index 000000000..abc0d4ba9 --- /dev/null +++ b/delphi-frontend/src/main/java/au/com/integradev/delphi/cfg/api/Cases.java @@ -0,0 +1,43 @@ +/* + * Sonar Delphi Plugin + * Copyright (C) 2025 Integrated Application Development + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package au.com.integradev.delphi.cfg.api; + +import java.util.Set; + +/** A block for the {@code case} statement's behaviour of a possible successor for each arm */ +public interface Cases extends Block, Terminated { + /** + * All the cases this statement can succeed to in the {@link ControlFlowGraph} + * + * @return the set of {@code case} arm successor blocks + */ + Set getCaseSuccessors(); + + /** + * Either the fallthrough {@code else} block or the next block in the {@link ControlFlowGraph} + * + * @return the fallthrough successor + */ + Block getFallthroughSuccessor(); + + @Override + default Set getSuccessors() { + return getCaseSuccessors(); + } +} diff --git a/delphi-frontend/src/main/java/au/com/integradev/delphi/cfg/api/ControlFlowGraph.java b/delphi-frontend/src/main/java/au/com/integradev/delphi/cfg/api/ControlFlowGraph.java new file mode 100644 index 000000000..adf01982c --- /dev/null +++ b/delphi-frontend/src/main/java/au/com/integradev/delphi/cfg/api/ControlFlowGraph.java @@ -0,0 +1,45 @@ +/* + * Sonar Delphi Plugin + * Copyright (C) 2025 Integrated Application Development + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package au.com.integradev.delphi.cfg.api; + +import java.util.List; + +/** A graph representation of all paths that could be traversed in the execution of code */ +public interface ControlFlowGraph { + /** + * The entry block to the control flow graph + * + * @return the starting block of the control flow graph + */ + Block getEntryBlock(); + + /** + * The final exit block of the control flow graph + * + * @return the final block of the control flow graph + */ + Block getExitBlock(); + + /** + * All the blocks within the control flow graph + * + * @return the list of all blocks in the control flow graph + */ + List getBlocks(); +} diff --git a/delphi-frontend/src/main/java/au/com/integradev/delphi/cfg/api/Finally.java b/delphi-frontend/src/main/java/au/com/integradev/delphi/cfg/api/Finally.java new file mode 100644 index 000000000..b73884cbf --- /dev/null +++ b/delphi-frontend/src/main/java/au/com/integradev/delphi/cfg/api/Finally.java @@ -0,0 +1,48 @@ +/* + * Sonar Delphi Plugin + * Copyright (C) 2025 Integrated Application Development + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package au.com.integradev.delphi.cfg.api; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +/** A {@code finally} block whose control flow depends on the existence of previous exceptions */ +public interface Finally extends Block { + /** + * Next block in the {@link ControlFlowGraph} without exceptional circumstances + * + * @return the successor block of the {@code finally} block + */ + Block getSuccessor(); + + /** + * Next block in the {@link ControlFlowGraph} if there were exceptional circumstances + * + * @return the successor block if the {@code finally} block was reached by an exception + */ + Block getExceptionSuccessor(); + + @Override + default Set getSuccessors() { + Set successors = new HashSet<>(); + successors.add(getSuccessor()); + successors.add(getExceptionSuccessor()); + return Collections.unmodifiableSet(successors); + } +} diff --git a/delphi-frontend/src/main/java/au/com/integradev/delphi/cfg/api/Halt.java b/delphi-frontend/src/main/java/au/com/integradev/delphi/cfg/api/Halt.java new file mode 100644 index 000000000..726ba24d9 --- /dev/null +++ b/delphi-frontend/src/main/java/au/com/integradev/delphi/cfg/api/Halt.java @@ -0,0 +1,22 @@ +/* + * Sonar Delphi Plugin + * Copyright (C) 2025 Integrated Application Development + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package au.com.integradev.delphi.cfg.api; + +/** A block representing a {@code Halt}, which is an immediate process termination */ +public interface Halt extends Terminus, Terminated {} diff --git a/delphi-frontend/src/main/java/au/com/integradev/delphi/cfg/api/Linear.java b/delphi-frontend/src/main/java/au/com/integradev/delphi/cfg/api/Linear.java new file mode 100644 index 000000000..4f612a52e --- /dev/null +++ b/delphi-frontend/src/main/java/au/com/integradev/delphi/cfg/api/Linear.java @@ -0,0 +1,36 @@ +/* + * Sonar Delphi Plugin + * Copyright (C) 2025 Integrated Application Development + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package au.com.integradev.delphi.cfg.api; + +import java.util.Set; + +/** A block which has a single successor */ +public interface Linear extends Block { + /** + * Next block in the {@link ControlFlowGraph} + * + * @return the successor block + */ + Block getSuccessor(); + + @Override + default Set getSuccessors() { + return Set.of(getSuccessor()); + } +} diff --git a/delphi-frontend/src/main/java/au/com/integradev/delphi/cfg/api/Terminated.java b/delphi-frontend/src/main/java/au/com/integradev/delphi/cfg/api/Terminated.java new file mode 100644 index 000000000..4680a83f3 --- /dev/null +++ b/delphi-frontend/src/main/java/au/com/integradev/delphi/cfg/api/Terminated.java @@ -0,0 +1,41 @@ +/* + * Sonar Delphi Plugin + * Copyright (C) 2025 Integrated Application Development + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package au.com.integradev.delphi.cfg.api; + +import au.com.integradev.delphi.cfg.block.TerminatorKind; +import org.sonar.plugins.communitydelphi.api.ast.DelphiNode; + +/** + * Some {@link Block}s are terminated by a particular control flow operation, e.g., a {@code goto}. + */ +public interface Terminated { + /** + * The node that terminates this block + * + * @return the terminator + */ + DelphiNode getTerminator(); + + /** + * The type of terminator for this block + * + * @return the kind of the terminator + */ + TerminatorKind getTerminatorKind(); +} diff --git a/delphi-frontend/src/main/java/au/com/integradev/delphi/cfg/api/Terminus.java b/delphi-frontend/src/main/java/au/com/integradev/delphi/cfg/api/Terminus.java new file mode 100644 index 000000000..f3db9210b --- /dev/null +++ b/delphi-frontend/src/main/java/au/com/integradev/delphi/cfg/api/Terminus.java @@ -0,0 +1,30 @@ +/* + * Sonar Delphi Plugin + * Copyright (C) 2025 Integrated Application Development + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package au.com.integradev.delphi.cfg.api; + +import java.util.Collections; +import java.util.Set; + +/** A block which has no successors by nature, e.g., the end of a routine */ +public interface Terminus extends Block { + @Override + default Set getSuccessors() { + return Collections.emptySet(); + } +} diff --git a/delphi-frontend/src/main/java/au/com/integradev/delphi/cfg/api/UnconditionalJump.java b/delphi-frontend/src/main/java/au/com/integradev/delphi/cfg/api/UnconditionalJump.java new file mode 100644 index 000000000..f3721ddc7 --- /dev/null +++ b/delphi-frontend/src/main/java/au/com/integradev/delphi/cfg/api/UnconditionalJump.java @@ -0,0 +1,43 @@ +/* + * Sonar Delphi Plugin + * Copyright (C) 2025 Integrated Application Development + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package au.com.integradev.delphi.cfg.api; + +import java.util.Set; + +/** A block which has particular alteration in control flow, e.g., {@code goto} */ +public interface UnconditionalJump extends Block, Terminated { + /** + * The jumping target block in the {@link ControlFlowGraph} + * + * @return the successor block of the jump + */ + Block getSuccessor(); + + /** + * The next block in the {@link ControlFlowGraph} if the jump were to be removed + * + * @return the successor block if this control flow alteration were to be removed + */ + Block getSuccessorIfRemoved(); + + @Override + default Set getSuccessors() { + return Set.of(getSuccessor()); + } +} diff --git a/delphi-frontend/src/main/java/au/com/integradev/delphi/cfg/api/UnknownException.java b/delphi-frontend/src/main/java/au/com/integradev/delphi/cfg/api/UnknownException.java new file mode 100644 index 000000000..26c51ea54 --- /dev/null +++ b/delphi-frontend/src/main/java/au/com/integradev/delphi/cfg/api/UnknownException.java @@ -0,0 +1,51 @@ +/* + * Sonar Delphi Plugin + * Copyright (C) 2025 Integrated Application Development + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package au.com.integradev.delphi.cfg.api; + +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * A block which may alter the control flow with an exception, e.g., routine invocations in a + * try-catch + * + *

Known exceptions with be directly specified with a {@code Linear} block to their target + */ +public interface UnknownException extends Block { + /** + * Next block without exceptional circumstances + * + * @return the successor block without exceptions + */ + Block getSuccessor(); + + /** + * Possible exit paths in exceptional circumstance + * + * @return the set of successor blocks that could be jumped to in the event of an exception + */ + Set getExceptions(); + + @Override + default Set getSuccessors() { + return Stream.concat(Stream.of(getSuccessor()), getExceptions().stream()) + .collect(Collectors.toSet()); + } +} diff --git a/delphi-frontend/src/main/java/au/com/integradev/delphi/cfg/block/BlockImpl.java b/delphi-frontend/src/main/java/au/com/integradev/delphi/cfg/block/BlockImpl.java new file mode 100644 index 000000000..a39d0936e --- /dev/null +++ b/delphi-frontend/src/main/java/au/com/integradev/delphi/cfg/block/BlockImpl.java @@ -0,0 +1,70 @@ +/* + * Sonar Delphi Plugin + * Copyright (C) 2025 Integrated Application Development + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package au.com.integradev.delphi.cfg.block; + +import au.com.integradev.delphi.cfg.api.Block; +import com.google.common.collect.Lists; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.sonar.plugins.communitydelphi.api.ast.DelphiNode; + +public abstract class BlockImpl implements Block { + private int id = 0; + private final List elements; + private final Set predecessors = new HashSet<>(); + + protected BlockImpl(List elements) { + this.elements = elements; + } + + @Override + public List getElements() { + return Collections.unmodifiableList(Lists.reverse(elements)); + } + + public void addPredecessor(Block predecessor) { + predecessors.add(predecessor); + } + + @Override + public Set getPredecessors() { + return Collections.unmodifiableSet(predecessors); + } + + public void setId(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public abstract void replaceInactiveSuccessor(Block inactiveBlock, Block target); + + protected static Block getNewTarget(Block subject, Block inactiveBlock, Block target) { + if (subject == inactiveBlock) { + return target; + } + return subject; + } + + public abstract String getDescription(); +} diff --git a/delphi-frontend/src/main/java/au/com/integradev/delphi/cfg/block/ProtoBlock.java b/delphi-frontend/src/main/java/au/com/integradev/delphi/cfg/block/ProtoBlock.java new file mode 100644 index 000000000..e2b7c3da9 --- /dev/null +++ b/delphi-frontend/src/main/java/au/com/integradev/delphi/cfg/block/ProtoBlock.java @@ -0,0 +1,60 @@ +/* + * Sonar Delphi Plugin + * Copyright (C) 2025 Integrated Application Development + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package au.com.integradev.delphi.cfg.block; + +import au.com.integradev.delphi.cfg.api.Block; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.function.BiConsumer; +import java.util.function.Function; +import org.sonar.plugins.communitydelphi.api.ast.DelphiNode; + +public class ProtoBlock { + private final List elements = new ArrayList<>(); + + // The construction and the population of the object is separated such that blocks can succeed to + // each other. + private Function, Block> blockSupplier; + private BiConsumer, Block> dataSetter; + + public ProtoBlock( + Function, Block> blockSupplier, + BiConsumer, Block> dataSetter) { + this.blockSupplier = blockSupplier; + this.dataSetter = dataSetter; + } + + public void addElement(DelphiNode element) { + this.elements.add(element); + } + + public void update(ProtoBlock block) { + this.blockSupplier = block.blockSupplier; + this.dataSetter = block.dataSetter; + } + + public Block createBlock() { + return blockSupplier.apply(this.elements); + } + + public void updateBlockData(Map blockMap) { + dataSetter.accept(blockMap, blockMap.get(this)); + } +} diff --git a/delphi-frontend/src/main/java/au/com/integradev/delphi/cfg/block/ProtoBlockFactory.java b/delphi-frontend/src/main/java/au/com/integradev/delphi/cfg/block/ProtoBlockFactory.java new file mode 100644 index 000000000..0e93e5507 --- /dev/null +++ b/delphi-frontend/src/main/java/au/com/integradev/delphi/cfg/block/ProtoBlockFactory.java @@ -0,0 +1,409 @@ +/* + * Sonar Delphi Plugin + * Copyright (C) 2025 Integrated Application Development + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package au.com.integradev.delphi.cfg.block; + +import au.com.integradev.delphi.cfg.api.Block; +import au.com.integradev.delphi.cfg.api.Branch; +import au.com.integradev.delphi.cfg.api.Cases; +import au.com.integradev.delphi.cfg.api.Finally; +import au.com.integradev.delphi.cfg.api.Halt; +import au.com.integradev.delphi.cfg.api.Linear; +import au.com.integradev.delphi.cfg.api.Terminus; +import au.com.integradev.delphi.cfg.api.UnconditionalJump; +import au.com.integradev.delphi.cfg.api.UnknownException; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import org.sonar.plugins.communitydelphi.api.ast.DelphiNode; + +public final class ProtoBlockFactory { + private ProtoBlockFactory() { + // Utility class + } + + public static ProtoBlock exitBlock() { + return new ProtoBlock(TerminusImpl::new, (blocks, block) -> {}); + } + + public static ProtoBlock halt(DelphiNode terminator) { + return new ProtoBlock(HaltImpl::new, (blocks, block) -> ((HaltImpl) block).setData(terminator)); + } + + public static ProtoBlock branch( + DelphiNode terminator, ProtoBlock trueBlock, ProtoBlock falseBlock) { + return new ProtoBlock( + BranchImpl::new, + (blocks, block) -> + ((BranchImpl) block) + .setData(terminator, blocks.get(trueBlock), blocks.get(falseBlock))); + } + + public static ProtoBlock finallyBlock(ProtoBlock successor, ProtoBlock finallySuccessor) { + return new ProtoBlock( + FinallyImpl::new, + (blocks, block) -> + ((FinallyImpl) block).setData(blocks.get(successor), blocks.get(finallySuccessor))); + } + + public static ProtoBlock linear(ProtoBlock successor) { + return new ProtoBlock( + LinearImpl::new, (blocks, block) -> ((LinearImpl) block).setData(blocks.get(successor))); + } + + public static ProtoBlock jump(DelphiNode terminator, ProtoBlock target, ProtoBlock withoutJump) { + return new ProtoBlock( + UnconditionalJumpImpl::new, + (blocks, block) -> + ((UnconditionalJumpImpl) block) + .setData(terminator, blocks.get(target), blocks.get(withoutJump))); + } + + public static ProtoBlock withExceptions(ProtoBlock successor, Set exceptions) { + return new ProtoBlock( + UnknownExceptionImpl::new, + (blocks, block) -> + ((UnknownExceptionImpl) block) + .setData( + blocks.get(successor), + exceptions.stream().map(blocks::get).collect(Collectors.toSet()))); + } + + public static ProtoBlock cases( + DelphiNode terminator, Set cases, ProtoBlock fallthrough) { + return new ProtoBlock( + CasesImpl::new, + (blocks, block) -> + ((CasesImpl) block) + .setData( + terminator, + cases.stream().map(blocks::get).collect(Collectors.toSet()), + blocks.get(fallthrough))); + } + + private static String getBlocksString(Collection block) { + return block.stream().map(ProtoBlockFactory::getBlockString).collect(Collectors.joining(" ")); + } + + private static String getBlockString(Block block) { + return "B" + ((BlockImpl) block).getId(); + } + + static class UnknownExceptionImpl extends BlockImpl implements UnknownException { + private Block successor; + private Set exceptions; + + public UnknownExceptionImpl(List elements) { + super(elements); + } + + public void setData(Block successor, Set exceptions) { + this.successor = successor; + this.exceptions = exceptions; + } + + @Override + public Block getSuccessor() { + return successor; + } + + @Override + public Set getExceptions() { + return Collections.unmodifiableSet(exceptions); + } + + @Override + public void replaceInactiveSuccessor(Block inactiveBlock, Block target) { + if (exceptions.remove(inactiveBlock)) { + exceptions.add(target); + } + this.successor = getNewTarget(this.successor, inactiveBlock, target); + } + + @Override + public String getDescription() { + return String.format( + "%n\tjumps to: %s%n\texceptions to: %s", + getBlockString(successor), getBlocksString(exceptions)); + } + } + + static class CasesImpl extends BlockImpl implements Cases { + private Terminator terminator; + private Set cases; + private Block fallthrough; + + public CasesImpl(List elements) { + super(elements); + } + + private void setData(DelphiNode terminator, Set cases, Block fallthrough) { + this.terminator = new Terminator(terminator); + this.cases = cases; + this.fallthrough = fallthrough; + } + + @Override + public Set getCaseSuccessors() { + return Collections.unmodifiableSet(cases); + } + + @Override + public Block getFallthroughSuccessor() { + return fallthrough; + } + + @Override + public DelphiNode getTerminator() { + return terminator.getTerminatorNode(); + } + + @Override + public TerminatorKind getTerminatorKind() { + return terminator.getKind(); + } + + @Override + public void replaceInactiveSuccessor(Block inactiveBlock, Block target) { + if (cases.remove(inactiveBlock)) { + cases.add(target); + } + this.fallthrough = getNewTarget(this.fallthrough, inactiveBlock, target); + } + + @Override + public String getDescription() { + return String.format( + "%n\tcases to: %s%n\tfallthrough to: %s", + getBlocksString(cases), getBlockString(fallthrough)); + } + } + + static class UnconditionalJumpImpl extends BlockImpl implements UnconditionalJump { + private Block target; + private Block withoutJump; + private Terminator terminator; + + public UnconditionalJumpImpl(List elements) { + super(elements); + } + + private void setData(DelphiNode terminator, Block target, Block withoutJump) { + this.terminator = new Terminator(terminator); + this.target = target; + this.withoutJump = withoutJump; + } + + @Override + public Block getSuccessor() { + return target; + } + + @Override + public Block getSuccessorIfRemoved() { + return withoutJump; + } + + @Override + public DelphiNode getTerminator() { + return terminator.getTerminatorNode(); + } + + @Override + public TerminatorKind getTerminatorKind() { + return terminator.getKind(); + } + + @Override + public void replaceInactiveSuccessor(Block inactiveBlock, Block target) { + this.target = getNewTarget(this.target, inactiveBlock, target); + this.withoutJump = getNewTarget(this.withoutJump, inactiveBlock, target); + } + + @Override + public String getDescription() { + return String.format( + "%n\tjumps to: %s%n\twithout jump to: %s", + getBlockString(target), getBlockString(withoutJump)); + } + } + + static class LinearImpl extends BlockImpl implements Linear { + private Block successor; + + protected LinearImpl(List elements) { + super(elements); + } + + public void setData(Block successor) { + this.successor = successor; + } + + @Override + public Block getSuccessor() { + return successor; + } + + @Override + public void replaceInactiveSuccessor(Block inactiveBlock, Block target) { + this.successor = getNewTarget(this.successor, inactiveBlock, target); + } + + @Override + public String getDescription() { + return String.format("%n\tjumps to: %s", getBlockString(successor)); + } + } + + static class FinallyImpl extends BlockImpl implements Finally { + private Block successor; + private Block exceptionSuccessor; + + protected FinallyImpl(List elements) { + super(elements); + } + + public void setData(Block successor, Block exitSuccessor) { + this.successor = successor; + this.exceptionSuccessor = exitSuccessor; + } + + @Override + public Block getSuccessor() { + return successor; + } + + @Override + public Block getExceptionSuccessor() { + return exceptionSuccessor; + } + + @Override + public void replaceInactiveSuccessor(Block inactiveBlock, Block target) { + this.successor = getNewTarget(this.successor, inactiveBlock, target); + this.exceptionSuccessor = getNewTarget(this.exceptionSuccessor, inactiveBlock, target); + } + + @Override + public String getDescription() { + return String.format( + "%n\tjumps to: %s%n\texits to: %s", + getBlockString(successor), getBlockString(exceptionSuccessor)); + } + } + + static class HaltImpl extends BlockImpl implements Halt { + private Terminator terminator; + + protected HaltImpl(List elements) { + super(elements); + } + + public void setData(DelphiNode terminator) { + this.terminator = new Terminator(terminator); + } + + @Override + public DelphiNode getTerminator() { + return terminator.getTerminatorNode(); + } + + @Override + public TerminatorKind getTerminatorKind() { + return terminator.getKind(); + } + + @Override + public void replaceInactiveSuccessor(Block inactiveBlock, Block target) { + // Block has no successors + } + + @Override + public String getDescription() { + return String.format("%n\tno successors"); + } + } + + private static class TerminusImpl extends BlockImpl implements Terminus { + + public TerminusImpl(List elements) { + super(elements); + } + + @Override + public void replaceInactiveSuccessor(Block inactiveBlock, Block target) { + // Block has no successors + } + + @Override + public String getDescription() { + return String.format("%n\t(Exit)"); + } + } + + static class BranchImpl extends BlockImpl implements Branch { + private Block trueBlock; + private Block falseBlock; + private Terminator terminator; + + public BranchImpl(List elements) { + super(elements); + } + + private void setData(DelphiNode terminator, Block trueBlock, Block falseBlock) { + this.terminator = new Terminator(terminator); + this.trueBlock = trueBlock; + this.falseBlock = falseBlock; + } + + @Override + public Block getTrueBlock() { + return trueBlock; + } + + @Override + public Block getFalseBlock() { + return falseBlock; + } + + @Override + public DelphiNode getTerminator() { + return terminator.getTerminatorNode(); + } + + @Override + public TerminatorKind getTerminatorKind() { + return terminator.getKind(); + } + + @Override + public void replaceInactiveSuccessor(Block inactiveBlock, Block target) { + this.trueBlock = getNewTarget(this.trueBlock, inactiveBlock, target); + this.falseBlock = getNewTarget(this.falseBlock, inactiveBlock, target); + } + + @Override + public String getDescription() { + return String.format( + "%n\tjumps to: %s(true) %s(false)", + getBlockString(trueBlock), getBlockString(falseBlock)); + } + } +} diff --git a/delphi-frontend/src/main/java/au/com/integradev/delphi/cfg/block/Terminator.java b/delphi-frontend/src/main/java/au/com/integradev/delphi/cfg/block/Terminator.java new file mode 100644 index 000000000..bfb466fc8 --- /dev/null +++ b/delphi-frontend/src/main/java/au/com/integradev/delphi/cfg/block/Terminator.java @@ -0,0 +1,70 @@ +/* + * Sonar Delphi Plugin + * Copyright (C) 2025 Integrated Application Development + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package au.com.integradev.delphi.cfg.block; + +import org.sonar.plugins.communitydelphi.api.ast.DelphiNode; +import org.sonar.plugins.communitydelphi.api.ast.GotoStatementNode; +import org.sonar.plugins.communitydelphi.api.ast.NameReferenceNode; +import org.sonar.plugins.communitydelphi.api.ast.RaiseStatementNode; +import org.sonar.plugins.communitydelphi.api.symbol.declaration.NameDeclaration; +import org.sonar.plugins.communitydelphi.api.symbol.declaration.RoutineNameDeclaration; + +public class Terminator { + private final TerminatorKind kind; + private final DelphiNode terminatorNode; + + public Terminator(DelphiNode terminator) { + this.kind = findTerminatorKind(terminator); + this.terminatorNode = terminator; + } + + private static TerminatorKind findTerminatorKind(DelphiNode terminator) { + if (terminator instanceof RaiseStatementNode) { + return TerminatorKind.RAISE; + } else if (terminator instanceof GotoStatementNode) { + return TerminatorKind.GOTO; + } else if (terminator instanceof NameReferenceNode) { + NameDeclaration nameDeclarationNode = + ((NameReferenceNode) terminator).getLastName().getNameDeclaration(); + if (nameDeclarationNode instanceof RoutineNameDeclaration) { + switch (((RoutineNameDeclaration) nameDeclarationNode).fullyQualifiedName()) { + case "System.Exit": + return TerminatorKind.EXIT; + case "System.Break": + return TerminatorKind.BREAK; + case "System.Halt": + return TerminatorKind.HALT; + case "System.Continue": + return TerminatorKind.CONTINUE; + default: + // fallthrough + } + } + } + return TerminatorKind.NODE; + } + + public TerminatorKind getKind() { + return kind; + } + + public DelphiNode getTerminatorNode() { + return terminatorNode; + } +} diff --git a/delphi-frontend/src/main/java/au/com/integradev/delphi/cfg/block/TerminatorKind.java b/delphi-frontend/src/main/java/au/com/integradev/delphi/cfg/block/TerminatorKind.java new file mode 100644 index 000000000..a767121e6 --- /dev/null +++ b/delphi-frontend/src/main/java/au/com/integradev/delphi/cfg/block/TerminatorKind.java @@ -0,0 +1,29 @@ +/* + * Sonar Delphi Plugin + * Copyright (C) 2025 Integrated Application Development + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package au.com.integradev.delphi.cfg.block; + +public enum TerminatorKind { + BREAK, + CONTINUE, + EXIT, + GOTO, + HALT, + NODE, + RAISE, +} diff --git a/delphi-frontend/src/main/java/org/sonar/plugins/communitydelphi/api/ast/CaseItemStatementNode.java b/delphi-frontend/src/main/java/org/sonar/plugins/communitydelphi/api/ast/CaseItemStatementNode.java index 7d834bb22..00ac35dc8 100644 --- a/delphi-frontend/src/main/java/org/sonar/plugins/communitydelphi/api/ast/CaseItemStatementNode.java +++ b/delphi-frontend/src/main/java/org/sonar/plugins/communitydelphi/api/ast/CaseItemStatementNode.java @@ -19,7 +19,11 @@ package org.sonar.plugins.communitydelphi.api.ast; import java.util.List; +import javax.annotation.Nullable; public interface CaseItemStatementNode extends StatementNode { List getExpressions(); + + @Nullable + StatementNode getStatement(); } diff --git a/delphi-frontend/src/main/java/org/sonar/plugins/communitydelphi/api/ast/CaseStatementNode.java b/delphi-frontend/src/main/java/org/sonar/plugins/communitydelphi/api/ast/CaseStatementNode.java index 832742f89..239afceef 100644 --- a/delphi-frontend/src/main/java/org/sonar/plugins/communitydelphi/api/ast/CaseStatementNode.java +++ b/delphi-frontend/src/main/java/org/sonar/plugins/communitydelphi/api/ast/CaseStatementNode.java @@ -22,6 +22,8 @@ import javax.annotation.Nullable; public interface CaseStatementNode extends StatementNode { + ExpressionNode getSelectorExpression(); + List getCaseItems(); @Nullable diff --git a/delphi-frontend/src/main/java/org/sonar/plugins/communitydelphi/api/ast/RepeatStatementNode.java b/delphi-frontend/src/main/java/org/sonar/plugins/communitydelphi/api/ast/RepeatStatementNode.java index 2bdc66150..f6f15632c 100644 --- a/delphi-frontend/src/main/java/org/sonar/plugins/communitydelphi/api/ast/RepeatStatementNode.java +++ b/delphi-frontend/src/main/java/org/sonar/plugins/communitydelphi/api/ast/RepeatStatementNode.java @@ -18,4 +18,8 @@ */ package org.sonar.plugins.communitydelphi.api.ast; -public interface RepeatStatementNode extends StatementNode {} +public interface RepeatStatementNode extends StatementNode { + ExpressionNode getGuardExpression(); + + StatementListNode getStatementList(); +} diff --git a/delphi-frontend/src/test/java/au/com/integradev/delphi/cfg/ControlFlowGraphTest.java b/delphi-frontend/src/test/java/au/com/integradev/delphi/cfg/ControlFlowGraphTest.java new file mode 100644 index 000000000..2c7a16c61 --- /dev/null +++ b/delphi-frontend/src/test/java/au/com/integradev/delphi/cfg/ControlFlowGraphTest.java @@ -0,0 +1,1431 @@ +/* + * Sonar Delphi Plugin + * Copyright (C) 2025 Integrated Application Development + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package au.com.integradev.delphi.cfg; + +import static au.com.integradev.delphi.cfg.checker.BlockChecker.block; +import static au.com.integradev.delphi.cfg.checker.BlockChecker.terminator; +import static au.com.integradev.delphi.cfg.checker.ElementChecker.element; +import static au.com.integradev.delphi.cfg.checker.GraphChecker.checker; +import static org.assertj.core.api.Assertions.*; + +import au.com.integradev.delphi.DelphiProperties; +import au.com.integradev.delphi.antlr.ast.visitors.SymbolAssociationVisitor; +import au.com.integradev.delphi.cfg.api.Block; +import au.com.integradev.delphi.cfg.api.ControlFlowGraph; +import au.com.integradev.delphi.cfg.api.Linear; +import au.com.integradev.delphi.cfg.api.Terminus; +import au.com.integradev.delphi.cfg.block.TerminatorKind; +import au.com.integradev.delphi.cfg.checker.GraphChecker; +import au.com.integradev.delphi.cfg.checker.StatementTerminator; +import au.com.integradev.delphi.compiler.Platform; +import au.com.integradev.delphi.file.DelphiFile; +import au.com.integradev.delphi.file.DelphiFileConfig; +import au.com.integradev.delphi.preprocessor.DelphiPreprocessorFactory; +import au.com.integradev.delphi.symbol.SymbolTable; +import au.com.integradev.delphi.type.factory.TypeFactoryImpl; +import au.com.integradev.delphi.utils.files.DelphiFileUtils; +import java.io.File; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.function.Consumer; +import org.apache.commons.io.FileUtils; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.sonar.plugins.communitydelphi.api.ast.BinaryExpressionNode; +import org.sonar.plugins.communitydelphi.api.ast.CaseStatementNode; +import org.sonar.plugins.communitydelphi.api.ast.CommonDelphiNode; +import org.sonar.plugins.communitydelphi.api.ast.DelphiNode; +import org.sonar.plugins.communitydelphi.api.ast.ForInStatementNode; +import org.sonar.plugins.communitydelphi.api.ast.ForToStatementNode; +import org.sonar.plugins.communitydelphi.api.ast.GotoStatementNode; +import org.sonar.plugins.communitydelphi.api.ast.IfStatementNode; +import org.sonar.plugins.communitydelphi.api.ast.IntegerLiteralNode; +import org.sonar.plugins.communitydelphi.api.ast.NameDeclarationNode; +import org.sonar.plugins.communitydelphi.api.ast.NameReferenceNode; +import org.sonar.plugins.communitydelphi.api.ast.NilLiteralNode; +import org.sonar.plugins.communitydelphi.api.ast.RaiseStatementNode; +import org.sonar.plugins.communitydelphi.api.ast.RealLiteralNode; +import org.sonar.plugins.communitydelphi.api.ast.RepeatStatementNode; +import org.sonar.plugins.communitydelphi.api.ast.RoutineImplementationNode; +import org.sonar.plugins.communitydelphi.api.ast.SimpleNameDeclarationNode; +import org.sonar.plugins.communitydelphi.api.ast.TextLiteralNode; +import org.sonar.plugins.communitydelphi.api.ast.TryStatementNode; +import org.sonar.plugins.communitydelphi.api.ast.UnaryExpressionNode; +import org.sonar.plugins.communitydelphi.api.ast.WhileStatementNode; +import org.sonar.plugins.communitydelphi.api.operator.BinaryOperator; +import org.sonar.plugins.communitydelphi.api.operator.UnaryOperator; + +class ControlFlowGraphTest { + private static final Logger LOG = LoggerFactory.getLogger(ControlFlowGraphTest.class); + + private ControlFlowGraph buildCfg(String input) { + return buildCfg(Collections.emptyMap(), input); + } + + private ControlFlowGraph buildCfg(List variables, String input) { + return buildCfg(Map.of("var", variables), input); + } + + private ControlFlowGraph buildCfg(Map> sections, String input) { + try { + var tempFile = File.createTempFile("CfgTest-", ".pas"); + tempFile.deleteOnExit(); + + StringBuilder content = new StringBuilder(); + content + .append("unit Test;\n") + .append("interface\n") + .append("uses System.SysUtils;\n") + .append("implementation\n") + .append("procedure TestProc;\n"); + for (Entry> section : sections.entrySet()) { + if (!section.getKey().isEmpty()) { + content.append(section.getKey()).append("\n"); + } + for (String declaration : section.getValue()) { + content.append(" ").append(declaration).append(";\n"); + } + } + content.append("begin\n").append(input).append("\nend;\n").append("end."); + + LOG.info("Test file:"); + LOG.info(content.toString()); + Files.write(tempFile.toPath(), content.toString().getBytes(StandardCharsets.UTF_8)); + + DelphiFileConfig config = DelphiFileUtils.mockConfig(); + var file = DelphiFile.from(tempFile, config); + + Path standardLibraryPath = createStandardLibrary(); + SymbolTable symbolTable = + SymbolTable.builder() + .preprocessorFactory(new DelphiPreprocessorFactory(Platform.WINDOWS)) + .typeFactory( + new TypeFactoryImpl( + DelphiProperties.COMPILER_TOOLCHAIN_DEFAULT, + DelphiProperties.COMPILER_VERSION_DEFAULT)) + .standardLibraryPath(standardLibraryPath) + .sourceFiles(List.of(file.getSourceCodeFile().toPath())) + .build(); + + FileUtils.deleteQuietly(standardLibraryPath.toFile()); + + new SymbolAssociationVisitor() + .visit(file.getAst(), new SymbolAssociationVisitor.Data(symbolTable)); + + var statementList = + file.getAst().findDescendantsOfType(RoutineImplementationNode.class).stream() + .filter(impl -> impl.getRoutineBody() != null) + .map(impl -> impl.getRoutineBody().getStatementBlock().getStatementList()) + .findFirst() + .orElseThrow(); + + var cfg = ControlFlowGraphFactory.create(statementList); + LOG.info(ControlFlowGraphDebug.toString(cfg)); + + return cfg; + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + private void test(String input, GraphChecker checker) { + test(Collections.emptyMap(), input, checker); + } + + private void test(List variables, String input, GraphChecker checker) { + checker.check(buildCfg(variables, input)); + } + + private void test(Map> sections, String input, GraphChecker checker) { + checker.check(buildCfg(sections, input)); + } + + private static Path createStandardLibrary() { + try { + Path bds = Files.createTempDirectory("bds"); + + var hook = new Thread(() -> FileUtils.deleteQuietly(bds.toFile())); + Runtime.getRuntime().addShutdownHook(hook); + + Path standardLibraryPath = Files.createDirectories(bds.resolve("source")); + Files.writeString( + standardLibraryPath.resolve("SysInit.pas"), + "unit SysInit;\ninterface\nimplementation\nend."); + Files.writeString( + standardLibraryPath.resolve("System.pas"), + "unit System;\n" + + "interface\n" + + "type\n" + + " TObject = class\n" + + " constructor Create;\n" + + " end;\n" + + " IInterface = interface\n" + + " end;\n" + + " TClassHelperBase = class\n" + + " end;\n" + + " TVarRec = record\n" + + " end;\n" + + "implementation\n" + + "end."); + Files.writeString( + standardLibraryPath.resolve("System.SysUtils.pas"), + "unit System.SysUtils;\n" + + "interface\n" + + "type\n" + + " Exception = class\n" + + " constructor Create(Message: String);\n" + + " end;\n" + + " EAbort = class(Exception);\n" + + "implementation\n" + + "end."); + + return bds; + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + private Consumer binaryOpTest(BinaryOperator operator) { + return node -> { + assertThat(node).as("node type").isInstanceOf(BinaryExpressionNode.class); + BinaryOperator actualOp = ((BinaryExpressionNode) node).getOperator(); + assertThat(actualOp).as("binary operator type").isEqualTo(operator); + }; + } + + private Consumer unaryOpTest(UnaryOperator operator) { + return node -> { + assertThat(node).as("node type").isInstanceOf(UnaryExpressionNode.class); + assertThat(((UnaryExpressionNode) node).getOperator()) + .as("unary operator type") + .isEqualTo(operator); + }; + } + + @Test + void testEmptyCfg() { + final ControlFlowGraph cfg = buildCfg(""); + checker().check(cfg); + assertThat(cfg.getEntryBlock().getSuccessors()).as("entry is an exit").isEmpty(); + } + + @Test + void testSimplestCfg() { + final ControlFlowGraph cfg = buildCfg("Foo;"); + checker(block(element(NameReferenceNode.class, "Foo")).succeedsTo(0)).check(cfg); + Block entry = cfg.getEntryBlock(); + assertThat(entry) + .withFailMessage("Expecting entry block to have single successor") + .isInstanceOf(Linear.class); + Block exit = entry.getSuccessors().iterator().next(); + assertThat(exit) + .withFailMessage("Expecting entry block's successor to be the exit block") + .isEqualTo(cfg.getExitBlock()) + .withFailMessage("Expecting entry block's successor to be of type Terminus.") + .isInstanceOf(Terminus.class); + } + + @Test + void testIfThen() { + test( + "if A then Foo;", + checker( + block(element(NameReferenceNode.class, "A")) + .branchesTo(1, 0) + .withTerminator(IfStatementNode.class), + block(element(NameReferenceNode.class, "Foo")).succeedsTo(0))); + } + + @Test + void testIfThenElse() { + test( + "if A then Foo else Bar;", + checker( + block(element(NameReferenceNode.class, "A")) + .branchesTo(2, 1) + .withTerminator(IfStatementNode.class), + block(element(NameReferenceNode.class, "Foo")).succeedsTo(0), + block(element(NameReferenceNode.class, "Bar")).succeedsTo(0))); + } + + @Test + void testIfThenElseIf() { + test( + "if A then Foo else if B then Bar;", + checker( + block(element(NameReferenceNode.class, "A")) + .branchesTo(3, 2) + .withTerminator(IfStatementNode.class), + block(element(NameReferenceNode.class, "Foo")).succeedsTo(0), + block(element(NameReferenceNode.class, "B")) + .branchesTo(1, 0) + .withTerminator(IfStatementNode.class), + block(element(NameReferenceNode.class, "Bar")).succeedsTo(0))); + } + + @Test + void testIfOr() { + test( + "if A or B then Foo;", + checker( + block(element(NameReferenceNode.class, "A")) + .branchesTo(1, 2) + .withTerminator(BinaryExpressionNode.class) + .withTerminatorNodeCheck(binaryOpTest(BinaryOperator.OR)), + block(element(NameReferenceNode.class, "B")) + .branchesTo(1, 0) + .withTerminator(IfStatementNode.class), + block(element(NameReferenceNode.class, "Foo")).succeedsTo(0))); + } + + @Test + void testIfAnd() { + test( + "if A and B then Foo;", + checker( + block(element(NameReferenceNode.class, "A")) + .branchesTo(2, 0) + .withTerminator(BinaryExpressionNode.class) + .withTerminatorNodeCheck(binaryOpTest(BinaryOperator.AND)), + block(element(NameReferenceNode.class, "B")) + .branchesTo(1, 0) + .withTerminator(IfStatementNode.class), + block(element(NameReferenceNode.class, "Foo")).succeedsTo(0))); + } + + @Test + void testLocalVarDeclaration() { + test( + "var Bar: TObject;", + checker(block(element(NameDeclarationNode.class, "Bar")).succeedsTo(0))); + } + + @Test + void testLocalVarListDeclaration() { + test( + "var Foo,Bar: TObject;", + checker( + block( + element(NameDeclarationNode.class, "Foo"), + element(NameDeclarationNode.class, "Bar")) + .succeedsTo(0))); + } + + @Test + void testLocalVarValueDeclaration() { + test( + "var Bar: TObject := nil;", + checker( + block(element(NilLiteralNode.class), element(NameDeclarationNode.class, "Bar")) + .succeedsTo(0))); + } + + @Test + void testUntypedConstDeclaration() { + test( + "const Foo = 0;", + checker( + block(element(IntegerLiteralNode.class), element(NameDeclarationNode.class, "Foo")) + .succeedsTo(0))); + } + + @Test + void testTypedConstDeclaration() { + test( + "const Foo: Integer = 0;", + checker( + block(element(IntegerLiteralNode.class), element(NameDeclarationNode.class, "Foo")) + .succeedsTo(0))); + } + + @Test + void testCaseStatement1Arm() { + test( + "case Foo of Bar: Bar1; end;", + checker( + block(element(NameReferenceNode.class, "Bar1")).succeedsTo(0), + block(element(NameReferenceNode.class, "Foo"), element(NameReferenceNode.class, "Bar")) + .succeedsToCases(0, 2) + .withTerminator(CaseStatementNode.class))); + } + + @Test + void testCaseStatement2Arm() { + test( + "case Foo of Bar: Bar1; Baz: Baz1; end;", + checker( + block(element(NameReferenceNode.class, "Bar1")).succeedsTo(0), + block(element(NameReferenceNode.class, "Baz1")).succeedsTo(0), + block( + element(NameReferenceNode.class, "Foo"), + element(NameReferenceNode.class, "Bar"), + element(NameReferenceNode.class, "Baz")) + .withTerminator(CaseStatementNode.class) + .succeedsToCases(0, 2, 3))); + } + + @Test + void testCaseStatementRangeArm() { + test( + "case Foo of 1..2: Bar1; 3..4: Baz1; end;", + checker( + block(element(NameReferenceNode.class, "Bar1")).succeedsTo(0), + block(element(NameReferenceNode.class, "Baz1")).succeedsTo(0), + block( + element(NameReferenceNode.class, "Foo"), + element(IntegerLiteralNode.class, "1"), + element(IntegerLiteralNode.class, "2"), + element(IntegerLiteralNode.class, "3"), + element(IntegerLiteralNode.class, "4")) + .withTerminator(CaseStatementNode.class) + .succeedsToCases(0, 2, 3))); + } + + @Test + void testCaseStatementElse() { + test( + "case Foo of Bar: Bar1; Baz: Baz1; else Flarp; end;", + checker( + block(element(NameReferenceNode.class, "Bar1")).succeedsTo(0), + block(element(NameReferenceNode.class, "Baz1")).succeedsTo(0), + block(element(NameReferenceNode.class, "Flarp")).succeedsTo(0), + block( + element(NameReferenceNode.class, "Foo"), + element(NameReferenceNode.class, "Bar"), + element(NameReferenceNode.class, "Baz")) + .withTerminator(CaseStatementNode.class) + .succeedsToCases(2, 3, 4))); + } + + @Test + void testRepeat() { + test( + "repeat Bar; until Foo;", + checker( + block(element(NameReferenceNode.class, "Bar")).succeedsTo(1), + block(element(NameReferenceNode.class, "Foo")) + .branchesTo(0, 2) + .withTerminator(RepeatStatementNode.class))); + } + + @Test + void testRepeatContinue() { + test( + "repeat Continue; Bar; until Foo;", + checker( + terminator(StatementTerminator.CONTINUE).jumpsTo(1, 2), + block(element(NameReferenceNode.class, "Bar")).succeedsTo(1), + block(element(NameReferenceNode.class, "Foo")) + .branchesTo(0, 3) + .withTerminator(RepeatStatementNode.class))); + } + + @Test + void testRepeatBreak() { + test( + "repeat Bar; break; until Foo;", + checker( + block(element(NameReferenceNode.class, "Bar")) + .jumpsTo(0, 1) + .withTerminator(StatementTerminator.BREAK), + block(element(NameReferenceNode.class, "Foo")) + .branchesTo(0, 2) + .withTerminator(RepeatStatementNode.class))); + } + + @Test + void testWhile() { + test( + "while Foo do Bar;", + checker( + block(element(NameReferenceNode.class, "Foo")) + .branchesTo(1, 0) + .withTerminator(WhileStatementNode.class), + block(element(NameReferenceNode.class, "Bar")).succeedsTo(2))); + } + + @Test + void testWhileContinue() { + test( + "while Foo do begin Continue; Bar; end;", + checker( + block(element(NameReferenceNode.class, "Foo")) + .branchesTo(2, 0) + .withTerminator(WhileStatementNode.class), + terminator(StatementTerminator.CONTINUE).jumpsTo(3, 1), + block(element(NameReferenceNode.class, "Bar")).succeedsTo(3))); + } + + @Test + void testWhileBreak() { + test( + "while Foo do begin Bar; break; end;", + checker( + block(element(NameReferenceNode.class, "Foo")) + .branchesTo(1, 0) + .withTerminator(WhileStatementNode.class), + block(element(NameReferenceNode.class, "Bar")) + .jumpsTo(0, 2) + .withTerminator(StatementTerminator.BREAK))); + } + + @Test + void testForToVarDecl() { + test( + "for var I := Foo to Bar do Baz;", + checker( + block(element(NameReferenceNode.class, "Foo")).succeedsTo(3), + block(element(NameReferenceNode.class, "Bar")).succeedsTo(1), + block(element(NameReferenceNode.class, "Baz")).succeedsTo(1), + block(element(NameDeclarationNode.class, "I")) + .branchesTo(2, 0) + .withTerminator(ForToStatementNode.class))); + } + + @Test + void testForToVarRef() { + test( + "for I := Foo to Bar do Baz;", + checker( + block(element(NameReferenceNode.class, "Foo")).succeedsTo(3), + block(element(NameReferenceNode.class, "Bar")).succeedsTo(1), + block(element(NameReferenceNode.class, "Baz")).succeedsTo(1), + block(element(NameReferenceNode.class, "I")) + .branchesTo(2, 0) + .withTerminator(ForToStatementNode.class))); + } + + @Test + void testForToBreak() { + test( + "for I := Foo to Bar do Break;", + checker( + block(element(NameReferenceNode.class, "Foo")).succeedsTo(3), + block(element(NameReferenceNode.class, "Bar")).succeedsTo(1), + terminator(StatementTerminator.BREAK).jumpsTo(0, 1), + block(element(NameReferenceNode.class, "I")) + .branchesTo(2, 0) + .withTerminator(ForToStatementNode.class))); + } + + @Test + void testForToConditionalBreak() { + test( + "for I := Foo to Bar do if I = 1 then Break;", + checker( + block(element(NameReferenceNode.class, "Foo")).succeedsTo(4), + block(element(NameReferenceNode.class, "Bar")).succeedsTo(1), + block( + element(NameReferenceNode.class, "I"), + element(IntegerLiteralNode.class, "1"), + element(BinaryExpressionNode.class) + .withCheck(binaryOpTest(BinaryOperator.EQUAL))) + .branchesTo(2, 1) + .withTerminator(IfStatementNode.class), + terminator(StatementTerminator.BREAK).jumpsTo(0, 1), + block(element(NameReferenceNode.class, "I")) + .branchesTo(3, 0) + .withTerminator(ForToStatementNode.class))); + } + + @Test + void testForToContinue() { + test( + "for I := Foo to Bar do Continue;", + checker( + block(element(NameReferenceNode.class, "Foo")).succeedsTo(3), + block(element(NameReferenceNode.class, "Bar")).succeedsTo(1), + terminator(StatementTerminator.CONTINUE).jumpsTo(1, 1), + block(element(NameReferenceNode.class, "I")) + .branchesTo(2, 0) + .withTerminator(ForToStatementNode.class))); + } + + @Test + void testForToConditionalContinue() { + test( + "for I := Foo to Bar do if I = 1 then Continue;", + checker( + block(element(NameReferenceNode.class, "Foo")).succeedsTo(4), + block(element(NameReferenceNode.class, "Bar")).succeedsTo(1), + block( + element(NameReferenceNode.class, "I"), + element(IntegerLiteralNode.class, "1"), + element(BinaryExpressionNode.class) + .withCheck(binaryOpTest(BinaryOperator.EQUAL))) + .branchesTo(2, 1) + .withTerminator(IfStatementNode.class), + terminator(StatementTerminator.CONTINUE).jumpsTo(1, 1), + block(element(NameReferenceNode.class, "I")) + .branchesTo(3, 0) + .withTerminator(ForToStatementNode.class))); + } + + @Test + void testForInVarDecl() { + test( + "for var I in List do Foo;", + checker( + block(element(NameReferenceNode.class, "List")).succeedsTo(1), + block(element(NameReferenceNode.class, "Foo")).succeedsTo(1), + block(element(NameDeclarationNode.class, "I")) + .branchesTo(2, 0) + .withTerminator(ForInStatementNode.class))); + } + + @Test + void testForInVarRef() { + test( + "for I in List do Foo;", + checker( + block(element(NameReferenceNode.class, "List")).succeedsTo(1), + block(element(NameReferenceNode.class, "Foo")).succeedsTo(1), + block(element(NameReferenceNode.class, "I")) + .branchesTo(2, 0) + .withTerminator(ForInStatementNode.class))); + } + + @Test + void testForInBreak() { + test( + "for I in List do Break;", + checker( + block(element(NameReferenceNode.class, "List")).succeedsTo(1), + terminator(StatementTerminator.BREAK).jumpsTo(0, 1), + block(element(NameReferenceNode.class, "I")) + .branchesTo(2, 0) + .withTerminator(ForInStatementNode.class))); + } + + @Test + void testForInConditionalBreak() { + test( + "for I in List do if I = 1 then Break;", + checker( + block(element(NameReferenceNode.class, "List")).succeedsTo(1), + block( + element(NameReferenceNode.class, "I"), + element(IntegerLiteralNode.class, "1"), + element(BinaryExpressionNode.class) + .withCheck(binaryOpTest(BinaryOperator.EQUAL))) + .branchesTo(2, 1) + .withTerminator(IfStatementNode.class), + terminator(StatementTerminator.BREAK).jumpsTo(0, 1), + block(element(NameReferenceNode.class, "I")) + .branchesTo(3, 0) + .withTerminator(ForInStatementNode.class))); + } + + @Test + void testForInContinue() { + test( + "for I in List do Continue;", + checker( + block(element(NameReferenceNode.class, "List")).succeedsTo(1), + terminator(StatementTerminator.CONTINUE).jumpsTo(1, 1), + block(element(NameReferenceNode.class, "I")) + .branchesTo(2, 0) + .withTerminator(ForInStatementNode.class))); + } + + @Test + void testForInConditionalContinue() { + test( + "for I in List do if I = 1 then Continue;", + checker( + block(element(NameReferenceNode.class, "List")).succeedsTo(1), + block( + element(NameReferenceNode.class, "I"), + element(IntegerLiteralNode.class, "1"), + element(BinaryExpressionNode.class) + .withCheck(binaryOpTest(BinaryOperator.EQUAL))) + .branchesTo(2, 1) + .withTerminator(IfStatementNode.class), + terminator(StatementTerminator.CONTINUE).jumpsTo(1, 1), + block(element(NameReferenceNode.class, "I")) + .branchesTo(3, 0) + .withTerminator(ForInStatementNode.class))); + } + + @Test + void testBreakOutsideOfLoop() { + GraphChecker checker = checker(); + assertThatThrownBy(() -> test("Break;", checker)) + .withFailMessage("'Break' statement not in loop statement.") + .isInstanceOf(IllegalStateException.class); + } + + @Test + void testContinueOutsideOfLoop() { + GraphChecker checker = checker(); + assertThatThrownBy(() -> test("Continue;", checker)) + .withFailMessage("'Continue' statement not in loop statement.") + .isInstanceOf(IllegalStateException.class); + } + + @Test + void testWith() { + test( + "with TObject.Create do Foo;", + checker( + block(element(NameReferenceNode.class, "TObject.Create")).succeedsTo(1), + block(element(NameReferenceNode.class, "Foo")).succeedsTo(0))); + } + + @Test + void testTryFinallyNoRaise() { + test( + Map.of("", List.of("procedure Foo; begin end")), + "try Foo finally Bar end;", + checker( + block(element(TryStatementNode.class)).succeedsTo(2), + block(element(NameReferenceNode.class, "Foo")).succeedsToWithExceptions(1, 1), + block(element(NameReferenceNode.class, "Bar")).succeedsToWithExit(0, 0))); + } + + @Test + void testTryFinallyRaise() { + test( + "try raise E finally Bar end;", + checker( + block(element(TryStatementNode.class)).succeedsTo(2), + block(element(NameReferenceNode.class, "E")) + .withTerminator(RaiseStatementNode.class, TerminatorKind.RAISE) + .jumpsTo(1, 1), + block(element(NameReferenceNode.class, "Bar")).succeedsToWithExit(0, 0))); + } + + @Test + void testTryFinallyExit() { + test( + "try Exit finally Bar end;", + checker( + block(element(TryStatementNode.class)).succeedsTo(2), + terminator(StatementTerminator.EXIT).jumpsTo(1, 1), + block(element(NameReferenceNode.class, "Bar")).succeedsToWithExit(0, 0))); + } + + @Test + void testTryFinallyBreak() { + test( + "while True do try Break finally Bar end;", + checker( + block(element(NameReferenceNode.class, "True")) + .branchesTo(3, 0) + .withTerminator(WhileStatementNode.class), + block(element(TryStatementNode.class)).succeedsTo(2), + terminator(StatementTerminator.BREAK).jumpsTo(1, 1), + block(element(NameReferenceNode.class, "Bar")).succeedsToWithExit(4, 0))); + } + + @Test + void testTryFinallyContinue() { + test( + "while True do try Continue finally Bar end;", + checker( + block(element(NameReferenceNode.class, "True")) + .branchesTo(3, 0) + .withTerminator(WhileStatementNode.class), + block(element(TryStatementNode.class)).succeedsTo(2), + terminator(StatementTerminator.CONTINUE).jumpsTo(1, 1), + block(element(NameReferenceNode.class, "Bar")).succeedsToWithExit(4, 0))); + } + + @Test + void testTryFinallyHalt() { + test( + "try Halt finally Bar end;", + checker( + block(element(TryStatementNode.class)).succeedsTo(2), + terminator(StatementTerminator.HALT).isSink(), + block(element(NameReferenceNode.class, "Bar")).succeedsToWithExit(0, 0))); + } + + @Test + void testTryContinueFinallyInLoop() { + test( + "for var A := 1 to 4 do begin" + + " try" + + " Continue;" + + " finally" + + " Bar;" + + " end;" + + "end;", + checker( + block(element(IntegerLiteralNode.class)).succeedsTo(5), + block(element(IntegerLiteralNode.class)).succeedsTo(1), + block(element(TryStatementNode.class)).succeedsTo(3), + terminator(StatementTerminator.CONTINUE).jumpsTo(2, 2), + block(element(NameReferenceNode.class, "Bar")).succeedsToWithExit(1, 0), + block(element(NameDeclarationNode.class, "A")) + .branchesTo(4, 0) + .withTerminator(ForToStatementNode.class))); + } + + @Test + void testTryBreakFinallyInLoop() { + test( + "for var A := 1 to 4 do begin" + + " try" + + " Break;" + + " finally" + + " Bar;" + + " end;" + + "end;", + checker( + block(element(IntegerLiteralNode.class)).succeedsTo(5), + block(element(IntegerLiteralNode.class)).succeedsTo(1), + block(element(TryStatementNode.class)).succeedsTo(3), + terminator(StatementTerminator.BREAK).jumpsTo(2, 2), + block(element(NameReferenceNode.class, "Bar")).succeedsToWithExit(1, 0), + block(element(NameDeclarationNode.class, "A")) + .branchesTo(4, 0) + .withTerminator(ForToStatementNode.class))); + } + + @Test + void testTryExitFinallyInLoop() { + test( + "for var A := 1 to 4 do begin" + + " try" + + " Exit;" + + " finally" + + " Bar;" + + " end;" + + "end;", + checker( + block(element(IntegerLiteralNode.class)).succeedsTo(5), + block(element(IntegerLiteralNode.class)).succeedsTo(1), + block(element(TryStatementNode.class)).succeedsTo(3), + terminator(StatementTerminator.EXIT).jumpsTo(2, 2), + block(element(NameReferenceNode.class, "Bar")).succeedsToWithExit(1, 0), + block(element(NameDeclarationNode.class, "A")) + .branchesTo(4, 0) + .withTerminator(ForToStatementNode.class))); + } + + @Test + void testTryHaltFinallyInLoop() { + test( + "for var A := 1 to 4 do begin" + + " try" + + " Halt;" + + " finally" + + " Bar;" + + " end;" + + "end;", + checker( + block(element(IntegerLiteralNode.class)).succeedsTo(5), + block(element(IntegerLiteralNode.class)).succeedsTo(1), + block(element(TryStatementNode.class)).succeedsTo(3), + terminator(StatementTerminator.HALT).isSink(), + block(element(NameReferenceNode.class, "Bar")).succeedsToWithExit(1, 0), + block(element(NameDeclarationNode.class, "A")) + .branchesTo(4, 0) + .withTerminator(ForToStatementNode.class))); + } + + @Test + void testTryExceptNoRaise() { + test( + Map.of("", List.of("procedure Foo; begin end", "procedure Bar; begin end")), + "try Foo finally Bar end;", + checker( + block(element(TryStatementNode.class)).succeedsTo(2), + block(element(NameReferenceNode.class, "Foo")).succeedsToWithExceptions(1, 1), + block(element(NameReferenceNode.class, "Bar")).succeedsToWithExit(0, 0))); + } + + @Test + void testTryBareExceptNoRaise() { + test( + Map.of("", List.of("procedure Foo; begin end")), + "try Foo; except Bar; end;", + checker( + block(element(TryStatementNode.class)).succeedsTo(2), + block(element(NameReferenceNode.class, "Foo")).succeedsToWithExceptions(0, 1), + block(element(NameReferenceNode.class, "Bar")).succeedsTo(0))); + } + + @Test + void testTryExceptOnNoRaise() { + test( + Map.of("", List.of("procedure Foo; begin end")), + "try\n" + + " Foo;\n" + + "except\n" + + " on E: EAbort do Bar;\n" + + " on E: Exception do Baz;\n" + + "end;", + checker( + block(element(TryStatementNode.class)).succeedsTo(3), + block(element(NameReferenceNode.class, "Foo")).succeedsToWithExceptions(0, 0, 1, 2), + block(element(NameDeclarationNode.class, "E"), element(NameReferenceNode.class, "Bar")) + .succeedsTo(0), + block(element(NameDeclarationNode.class, "E"), element(NameReferenceNode.class, "Baz")) + .succeedsTo(0))); + } + + @Test + void testTryExceptRaise1stCatch() { + test( + Map.of( + "", + List.of( + "procedure Foo; begin end", + "procedure Bar; begin end", + "procedure Baz; begin end")), + "try\n" + + " raise EAbort.Create('');\n" + + "except\n" + + " on E: EAbort do Bar;\n" + + " on E: Exception do Baz;\n" + + "end;", + checker( + block(element(TryStatementNode.class)).succeedsTo(4), + block( + element(TextLiteralNode.class, "''"), + element(NameReferenceNode.class, "EAbort.Create")) + .succeedsToWithExceptions(3, 0, 1, 2), + terminator(RaiseStatementNode.class, TerminatorKind.RAISE).jumpsTo(2, 0), + block(element(NameDeclarationNode.class, "E"), element(NameReferenceNode.class, "Bar")) + .succeedsTo(0), + block(element(NameDeclarationNode.class, "E"), element(NameReferenceNode.class, "Baz")) + .succeedsTo(0))); + } + + @Test + void testTryExceptRaise2ndCatch() { + test( + Map.of( + "", + List.of( + "procedure Foo; begin end", + "procedure Bar; begin end", + "procedure Baz; begin end")), + "try\n" + + " raise Exception.Create('');\n" + + "except\n" + + " on E: EAbort do Bar;\n" + + " on E: Exception do Baz;\n" + + "end;", + checker( + block(element(TryStatementNode.class)).succeedsTo(4), + block( + element(TextLiteralNode.class, "''"), + element(NameReferenceNode.class, "Exception.Create")) + .succeedsToWithExceptions(3, 0, 1, 2), + terminator(RaiseStatementNode.class, TerminatorKind.RAISE).jumpsTo(1, 0), + block(element(NameDeclarationNode.class, "E"), element(NameReferenceNode.class, "Bar")) + .succeedsTo(0), + block(element(NameDeclarationNode.class, "E"), element(NameReferenceNode.class, "Baz")) + .succeedsTo(0))); + } + + @Test + void testTryBareExceptRaise() { + test( + Map.of("", List.of("procedure Bar; begin end")), + "try raise Exception.Create(''); except Bar; end;", + checker( + block(element(TryStatementNode.class)).succeedsTo(3), + block( + element(TextLiteralNode.class, "''"), + element(NameReferenceNode.class, "Exception.Create")) + .succeedsToWithExceptions(2, 1), + terminator(RaiseStatementNode.class, TerminatorKind.RAISE).jumpsTo(1, 0), + block(element(NameReferenceNode.class, "Bar")).succeedsTo(0))); + } + + @Test + void testTryExceptElseRaise() { + test( + Map.of( + "", + List.of( + "procedure Foo; begin end", + "procedure Bar; begin end", + "procedure Baz; begin end")), + "try\n" + + " raise TObject.Create;\n" + + "except\n" + + " on E: EAbort do Bar;\n" + + " on E: Exception do Baz;\n" + + "else\n" + + " Flarp;\n" + + "end;", + checker( + block(element(TryStatementNode.class)).succeedsTo(5), + block(element(NameReferenceNode.class, "TObject.Create")) + .succeedsToWithExceptions(4, 1, 2, 3), + terminator(RaiseStatementNode.class, TerminatorKind.RAISE).jumpsTo(1, 0), + block(element(NameDeclarationNode.class, "E"), element(NameReferenceNode.class, "Bar")) + .succeedsTo(0), + block(element(NameDeclarationNode.class, "E"), element(NameReferenceNode.class, "Baz")) + .succeedsTo(0), + block(element(NameReferenceNode.class, "Flarp")).succeedsTo(0))); + } + + @Test + void testNestedTryExceptFinally() { + test( + Map.of( + "", + List.of( + "procedure Foo; begin end", + "procedure Bar; begin end", + "procedure Baz; begin end")), + "try\n" + + " try\n" + + " Foo\n" + + " except\n" + + " on E: Exception do Bar\n" + + " end\n" + + "finally\n" + + " Baz\n" + + "end;", + checker( + block(element(TryStatementNode.class)).succeedsTo(4), + block(element(TryStatementNode.class)).succeedsTo(3), + block(element(NameReferenceNode.class, "Foo")).succeedsToWithExceptions(1, 1, 2), + block(element(NameDeclarationNode.class, "E"), element(NameReferenceNode.class, "Bar")) + .succeedsToWithExceptions(1, 1), + block(element(NameReferenceNode.class, "Baz")).succeedsToWithExit(0, 0))); + } + + @Test + void testNestedTryFinallyExcept() { + test( + Map.of( + "", + List.of( + "procedure Foo; begin end", + "procedure Bar; begin end", + "procedure Baz; begin end")), + "try try Foo finally Bar end except on E: Exception do Baz end;", + checker( + block(element(TryStatementNode.class)).succeedsTo(4), + block(element(TryStatementNode.class)).succeedsTo(3), + block(element(NameReferenceNode.class, "Foo")).succeedsToWithExceptions(2, 2), + block(element(NameReferenceNode.class, "Bar")).succeedsToWithExceptions(0, 0, 1), + block(element(NameDeclarationNode.class, "E"), element(NameReferenceNode.class, "Baz")) + .succeedsTo(0))); + } + + @Test + void testTryBareExcept() { + test( + Map.of("", List.of("procedure Foo; begin end")), + "try raise Exception.Create('') except Foo end;", + checker( + block(element(TryStatementNode.class)).succeedsTo(3), + block( + element(TextLiteralNode.class, "''"), + element(NameReferenceNode.class, "Exception.Create")) + .succeedsToWithExceptions(2, 1), + terminator(RaiseStatementNode.class, TerminatorKind.RAISE).jumpsTo(1, 0), + block(element(NameReferenceNode.class, "Foo")).succeedsTo(0))); + } + + @Test + void testTryBareExceptReRaise() { + test( + Map.of("", List.of("procedure Foo; begin end")), + "try Foo except raise; end;", + checker( + block(element(TryStatementNode.class)).succeedsTo(2), + block(element(NameReferenceNode.class, "Foo")).succeedsToWithExceptions(0, 1), + block(element(RaiseStatementNode.class)).succeedsToWithExceptions(0))); + } + + @Test + void testTryExceptReRaise() { + test( + Map.of("", List.of("procedure Foo; begin end")), + "try Foo except on E: Exception do raise; end;", + checker( + block(element(TryStatementNode.class)).succeedsTo(2), + block(element(NameReferenceNode.class, "Foo")).succeedsToWithExceptions(0, 0, 1), + block(element(NameDeclarationNode.class, "E"), element(RaiseStatementNode.class)) + .succeedsToWithExceptions(0))); + } + + @Test + void testRaiseOutsideTry() { + test( + "raise A;", + checker( + block(element(NameReferenceNode.class, "A")) + .jumpsTo(0, 0) + .withTerminator(RaiseStatementNode.class, TerminatorKind.RAISE))); + } + + @Test + void testCompoundStatement() { + test( + "begin Foo; end; begin Bar; end;", + checker( + block(element(NameReferenceNode.class, "Foo"), element(NameReferenceNode.class, "Bar")) + .succeedsTo(0))); + } + + @Test + void testAssignmentAnd() { + test( + List.of("Foo: Boolean", "A: Boolean", "B: Boolean"), + "Foo := A and B;", + checker( + block(element(NameReferenceNode.class, "A")) + .branchesTo(2, 1) + .withTerminator(BinaryExpressionNode.class), + block(element(NameReferenceNode.class, "B")).succeedsTo(1), + block(element(NameReferenceNode.class, "Foo")).succeedsTo(0))); + } + + @Test + void testAssignmentPlus() { + test( + List.of("Foo: Integer", "A: Integer"), + "Foo := A + 1.1;", + checker( + block( + element(NameReferenceNode.class, "A"), + element(RealLiteralNode.class, "1.1"), + element(BinaryExpressionNode.class), + element(NameReferenceNode.class, "Foo")) + .succeedsTo(0))); + } + + @Test + void testAssignmentMultipleBinary() { + test( + List.of("Foo: Integer", "A: Integer", "B: Integer", "C: Integer", "D: Integer"), + "Foo := A + B / C * D;", + checker( + block( + element(NameReferenceNode.class, "A"), + element(NameReferenceNode.class, "B"), + element(NameReferenceNode.class, "C"), + element(BinaryExpressionNode.class) + .withCheck(binaryOpTest(BinaryOperator.DIVIDE)), + element(NameReferenceNode.class, "D"), + element(BinaryExpressionNode.class) + .withCheck(binaryOpTest(BinaryOperator.MULTIPLY)), + element(BinaryExpressionNode.class).withCheck(binaryOpTest(BinaryOperator.ADD)), + element(NameReferenceNode.class, "Foo")) + .succeedsTo(0))); + } + + @Test + void testArrayConstructor() { + test( + "Foo := [1, 2, 3, 4, 5];", + checker( + block( + element(IntegerLiteralNode.class, "1"), + element(IntegerLiteralNode.class, "2"), + element(IntegerLiteralNode.class, "3"), + element(IntegerLiteralNode.class, "4"), + element(IntegerLiteralNode.class, "5"), + element(NameReferenceNode.class, "Foo")) + .succeedsTo(0))); + } + + @Test + void testExitStatement() { + test( + List.of("Foo: TObject"), + "if Foo = nil then Exit;", + checker( + block( + element(NameReferenceNode.class, "Foo"), + element(NilLiteralNode.class), + element(BinaryExpressionNode.class) + .withCheck(binaryOpTest(BinaryOperator.EQUAL))) + .branchesTo(1, 0) + .withTerminator(IfStatementNode.class), + terminator(StatementTerminator.EXIT).jumpsTo(0, 0))); + } + + @Test + void testExitValueStatement() { + test( + List.of("Foo: TObject", "Bar: TObject"), + "if Foo = nil then Exit(Bar);", + checker( + block( + element(NameReferenceNode.class, "Foo"), + element(NilLiteralNode.class), + element(BinaryExpressionNode.class) + .withCheck(binaryOpTest(BinaryOperator.EQUAL))) + .branchesTo(1, 0) + .withTerminator(IfStatementNode.class), + block(element(NameReferenceNode.class, "Bar")) + .withTerminator(StatementTerminator.EXIT) + .jumpsTo(0, 0))); + } + + @Test + void testExitCascadedOr() { + test( + List.of("A, B, C: Boolean"), + "Exit(A or B or C);", + checker( + block(element(NameReferenceNode.class, "A")) + .branchesTo(3, 4) + .withTerminator(BinaryExpressionNode.class) + .withTerminatorNodeCheck(binaryOpTest(BinaryOperator.OR)), + block(element(NameReferenceNode.class, "B")).succeedsTo(3), + terminator(BinaryExpressionNode.class) + .withTerminatorNodeCheck(binaryOpTest(BinaryOperator.OR)) + .branchesTo(1, 2), + block(element(NameReferenceNode.class, "C")).succeedsTo(1), + terminator(StatementTerminator.EXIT).jumpsTo(0, 0))); + } + + @Test + void testExitCascadedAnd() { + test( + List.of("A, B, C: Boolean"), + "Exit(A and B and C);", + checker( + block(element(NameReferenceNode.class, "A")) + .withTerminator(BinaryExpressionNode.class) + .withTerminatorNodeCheck(binaryOpTest(BinaryOperator.AND)) + .branchesTo(4, 3), + block(element(NameReferenceNode.class, "B")).succeedsTo(3), + terminator(BinaryExpressionNode.class) + .withTerminatorNodeCheck(binaryOpTest(BinaryOperator.AND)) + .branchesTo(2, 1), + block(element(NameReferenceNode.class, "C")).succeedsTo(1), + terminator(StatementTerminator.EXIT).jumpsTo(0, 0))); + } + + @Test + void testExitComplexBoolean() { + test( + List.of("Bool, A, B: Boolean"), + "Exit((not Bool and A) or (Bool and B));", + checker( + block( + element(NameReferenceNode.class, "Bool"), + element(UnaryExpressionNode.class).withCheck(unaryOpTest(UnaryOperator.NOT))) + .branchesTo(5, 4) + .withTerminator(BinaryExpressionNode.class) + .withTerminatorNodeCheck(binaryOpTest(BinaryOperator.AND)), + block(element(NameReferenceNode.class, "A")).succeedsTo(4), + terminator(BinaryExpressionNode.class) + .withTerminatorNodeCheck(binaryOpTest(BinaryOperator.OR)) + .branchesTo(1, 3), + block(element(NameReferenceNode.class, "Bool")) + .withTerminator(BinaryExpressionNode.class) + .withTerminatorNodeCheck(binaryOpTest(BinaryOperator.AND)) + .branchesTo(2, 1), + block(element(NameReferenceNode.class, "B")).succeedsTo(1), + terminator(StatementTerminator.EXIT).jumpsTo(0, 0))); + } + + @Test + void testBareInherited() { + test("inherited;", checker(block(element(CommonDelphiNode.class, "inherited")).succeedsTo(0))); + } + + @Test + void testNamedInheritedNoArgs() { + test( + "inherited Foo;", + checker( + block( + element(CommonDelphiNode.class, "inherited"), + element(NameReferenceNode.class, "Foo")) + .succeedsTo(0))); + } + + @Test + void testInherited() { + test( + "inherited Foo(A, B, C);", + checker( + block( + element(CommonDelphiNode.class, "inherited"), + element(NameReferenceNode.class, "Foo"), + element(NameReferenceNode.class, "A"), + element(NameReferenceNode.class, "B"), + element(NameReferenceNode.class, "C")) + .succeedsTo(0))); + } + + @Test + void testSucceedingLabelGoto() { + test( + Map.of("label", List.of("A")), + "if B then goto A; A:", + checker( + block(element(NameReferenceNode.class, "B")) + .branchesTo(1, 0) + .withTerminator(IfStatementNode.class), + terminator(GotoStatementNode.class, TerminatorKind.GOTO).jumpsTo(0, 0))); + } + + @Test + void testPrecedingLabelGoto() { + test( + Map.of("label", List.of("A")), + "A: if B then goto A;", + checker( + block(element(NameReferenceNode.class, "B")) + .branchesTo(1, 0) + .withTerminator(IfStatementNode.class), + block(element(NameReferenceNode.class, "A")) + .withTerminator(GotoStatementNode.class, TerminatorKind.GOTO) + .jumpsTo(2, 0))); + } + + @Test + void testLabelSeparatesBlock() { + test( + Map.of("label", List.of("A")), + "Foo; A: Bar; if B then goto A;", + checker( + block(element(NameReferenceNode.class, "Foo")).succeedsTo(2), + block(element(NameReferenceNode.class, "Bar"), element(NameReferenceNode.class, "B")) + .branchesTo(1, 0) + .withTerminator(IfStatementNode.class), + block(element(NameReferenceNode.class, "A")) + .withTerminator(GotoStatementNode.class, TerminatorKind.GOTO) + .jumpsTo(2, 0))); + } + + @Test + void testAnonymousRoutinesAreIgnored() { + test( + "A := procedure begin Foo; Bar; end;", + checker(block(element(NameReferenceNode.class, "A")).succeedsTo(0))); + } + + @Test + void testInlineAssemblyIsIgnored() { + test( + "Foo; asm XOR EAX, EAX end;", + checker(block(element(NameReferenceNode.class, "Foo")).succeedsTo(0))); + } + + @Test + void testComplexConstructions() { + test( + Map.of("label", List.of("Label1")), + "Foo1;\n" + + "with TObject.Create do begin\n" + + " WithBar1;\n" + + " WithBar2;\n" + + "end;\n" + + "Foo2;\n" + + "for var A := 1 + +1 to 2 + 2 do begin\n" + + " ForBar1;\n" + + " ForBar2;\n" + + " if A = 4 then goto Label1;\n" + + "end;\n" + + "Foo3;\n" + + "while A and B or C do begin\n" + + " WhileBar1;\n" + + " Label1:\n" + + " WhileBar2;\n" + + " try\n" + + " Break;\n" + + " finally\n" + + " var X := '123';\n" + + " if D or E then X := 'a'\n" + + " else X := 'b';\n" + + " end;\n" + + "end;\n" + + "Foo4;", + checker( + block(element(NameReferenceNode.class, "Foo1")).succeedsTo(21), + block(element(NameReferenceNode.class, "TObject.Create")).succeedsTo(20), + block( + element(NameReferenceNode.class, "WithBar1"), + element(NameReferenceNode.class, "WithBar2")) + .succeedsTo(19), + block(element(NameReferenceNode.class, "Foo2")).succeedsTo(18), + block( + element(IntegerLiteralNode.class, "1"), + element(IntegerLiteralNode.class, "1"), + element(UnaryExpressionNode.class).withCheck(unaryOpTest(UnaryOperator.PLUS)), + element(BinaryExpressionNode.class).withCheck(binaryOpTest(BinaryOperator.ADD))) + .succeedsTo(17), + block( + element(IntegerLiteralNode.class, "2"), + element(IntegerLiteralNode.class, "2"), + element(BinaryExpressionNode.class).withCheck(binaryOpTest(BinaryOperator.ADD))) + .succeedsTo(14), + block( + element(NameReferenceNode.class, "ForBar1"), + element(NameReferenceNode.class, "ForBar2"), + element(NameReferenceNode.class, "A"), + element(IntegerLiteralNode.class, "4"), + element(BinaryExpressionNode.class) + .withCheck(binaryOpTest(BinaryOperator.EQUAL))) + .branchesTo(15, 14) + .withTerminator(IfStatementNode.class), + block().jumpsTo(8, 14).withTerminator(GotoStatementNode.class, TerminatorKind.GOTO), + block(element(SimpleNameDeclarationNode.class, "A")) + .branchesTo(16, 13) + .withTerminator(ForToStatementNode.class), + block(element(NameReferenceNode.class, "Foo3")).succeedsTo(12), + block(element(NameReferenceNode.class, "A")) + .branchesTo(11, 10) + .withTerminator(BinaryExpressionNode.class) + .withTerminatorNodeCheck(binaryOpTest(BinaryOperator.AND)), + block(element(NameReferenceNode.class, "B")) + .branchesTo(9, 10) + .withTerminator(BinaryExpressionNode.class) + .withTerminatorNodeCheck(binaryOpTest(BinaryOperator.OR)), + block(element(NameReferenceNode.class, "C")) + .branchesTo(9, 1) + .withTerminator(WhileStatementNode.class), + block(element(NameReferenceNode.class, "WhileBar1")).succeedsTo(8), + block(element(NameReferenceNode.class, "WhileBar2"), element(TryStatementNode.class)) + .succeedsTo(7), + block().jumpsTo(6, 6).withTerminator(NameReferenceNode.class, TerminatorKind.BREAK), + block( + element(TextLiteralNode.class, "'123'"), + element(SimpleNameDeclarationNode.class, "X"), + element(NameReferenceNode.class, "D")) + .branchesTo(4, 5) + .withTerminator(BinaryExpressionNode.class) + .withTerminatorNodeCheck(binaryOpTest(BinaryOperator.OR)), + block(element(NameReferenceNode.class, "E")) + .branchesTo(4, 3) + .withTerminator(IfStatementNode.class), + block(element(TextLiteralNode.class, "'a'"), element(NameReferenceNode.class, "X")) + .succeedsTo(2), + block(element(TextLiteralNode.class, "'b'"), element(NameReferenceNode.class, "X")) + .succeedsTo(2), + block().succeedsToWithExit(12, 0), + block(element(NameReferenceNode.class, "Foo4")).succeedsTo(0))); + } +} diff --git a/delphi-frontend/src/test/java/au/com/integradev/delphi/cfg/checker/BlockChecker.java b/delphi-frontend/src/test/java/au/com/integradev/delphi/cfg/checker/BlockChecker.java new file mode 100644 index 000000000..2e3f12175 --- /dev/null +++ b/delphi-frontend/src/test/java/au/com/integradev/delphi/cfg/checker/BlockChecker.java @@ -0,0 +1,327 @@ +/* + * Sonar Delphi Plugin + * Copyright (C) 2025 Integrated Application Development + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package au.com.integradev.delphi.cfg.checker; + +import static org.assertj.core.api.Assertions.*; + +import au.com.integradev.delphi.cfg.api.Block; +import au.com.integradev.delphi.cfg.api.Branch; +import au.com.integradev.delphi.cfg.api.Cases; +import au.com.integradev.delphi.cfg.api.Finally; +import au.com.integradev.delphi.cfg.api.Halt; +import au.com.integradev.delphi.cfg.api.Linear; +import au.com.integradev.delphi.cfg.api.Terminated; +import au.com.integradev.delphi.cfg.api.UnconditionalJump; +import au.com.integradev.delphi.cfg.api.UnknownException; +import au.com.integradev.delphi.cfg.block.BlockImpl; +import au.com.integradev.delphi.cfg.block.TerminatorKind; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import org.sonar.plugins.communitydelphi.api.ast.DelphiNode; +import org.sonar.plugins.communitydelphi.api.ast.NameReferenceNode; +import org.sonar.plugins.communitydelphi.api.symbol.declaration.RoutineNameDeclaration; + +public class BlockChecker { + private BlockDetailChecker successorChecker = null; + private BlockDetailChecker terminatorChecker = null; + private final List terminatorNodeChecks = new ArrayList<>(); + private final List elementCheckers = new ArrayList<>(); + + public static BlockChecker block(ElementChecker... elementCheckers) { + return new BlockChecker(elementCheckers); + } + + public static BlockChecker terminator(Class terminatorClass) { + return new BlockChecker().withTerminator(terminatorClass); + } + + public static BlockChecker terminator( + Class terminatorClass, TerminatorKind kind) { + return new BlockChecker().withTerminator(terminatorClass, kind); + } + + public static BlockChecker terminator(StatementTerminator terminatorClass) { + return new BlockChecker().withTerminator(terminatorClass); + } + + private BlockChecker(ElementChecker... elementCheckers) { + Collections.addAll(this.elementCheckers, elementCheckers); + } + + public void check(final Block block) { + assertThat(block.getElements()).as("elements count").hasSize(elementCheckers.size()); + for (int elementId = 0; elementId < elementCheckers.size(); elementId++) { + elementCheckers + .get(elementId) + .withBlockId(((BlockImpl) block).getId(), elementId) + .check(block.getElements().get(elementId)); + } + assertThat(successorChecker) + .withFailMessage("%s should have its successors declared", getBlockDisplay(block)) + .isNotNull(); + successorChecker.check(block); + + if (terminatorChecker != null) { + terminatorChecker.check(block); + } else { + assertThat(block.getSuccessors()) + .withFailMessage("%s should have its terminator specified", getBlockDisplay(block)) + .isNotInstanceOf(Terminated.class); + } + terminatorNodeChecks.forEach(check -> check.check(block)); + } + + public BlockChecker succeedsTo(int successor) { + this.successorChecker = + new BlockDetailChecker( + block -> { + Linear branch = assertBlockIsType(block, Linear.class); + assertThat(getBlockId(branch.getSuccessor())) + .withFailMessage( + getBlockDisplay(block) + " is expected to have successor of B" + successor) + .isEqualTo(successor); + }); + return this; + } + + public BlockChecker succeedsToWithExit(int successor, int exitSuccessor) { + this.successorChecker = + new BlockDetailChecker( + block -> { + Finally branch = assertBlockIsType(block, Finally.class); + assertThat(getBlockId(branch.getSuccessor())) + .withFailMessage( + getBlockDisplay(block) + " is expected to have successor of B" + successor) + .isEqualTo(successor); + assertThat(getBlockId(branch.getExceptionSuccessor())) + .withFailMessage( + getBlockDisplay(block) + + " is expected to have exit successor of B" + + exitSuccessor) + .isEqualTo(exitSuccessor); + }); + return this; + } + + public BlockChecker branchesTo(int trueBlock, int falseBlock) { + successorChecker = + new BlockDetailChecker( + block -> { + Branch branch = assertBlockIsType(block, Branch.class); + assertThat(getBlockId(branch.getTrueBlock())) + .withFailMessage( + getBlockDisplay(block) + + " is expected to have true successor of B" + + trueBlock) + .isEqualTo(trueBlock); + assertThat(getBlockId(branch.getFalseBlock())) + .withFailMessage( + getBlockDisplay(block) + + " is expected to have false successor of B" + + falseBlock) + .isEqualTo(falseBlock); + }); + return this; + } + + public BlockChecker jumpsTo(int successor, int successorWithoutJump) { + this.successorChecker = + new BlockDetailChecker( + block -> { + UnconditionalJump branch = assertBlockIsType(block, UnconditionalJump.class); + assertThat(getBlockId(branch.getSuccessor())) + .withFailMessage( + getBlockDisplay(block) + " is expected to have successor of B" + successor) + .isEqualTo(successor); + assertThat(getBlockId(branch.getSuccessorIfRemoved())) + .withFailMessage( + getBlockDisplay(block) + + " is expected to have successor without jump of B" + + successorWithoutJump) + .isEqualTo(successorWithoutJump); + }); + return this; + } + + public BlockChecker succeedsToCases(int fallthrough, int... cases) { + Set expectedCases = Arrays.stream(cases).boxed().collect(Collectors.toSet()); + this.successorChecker = + new BlockDetailChecker( + block -> { + Cases caseSuccessor = assertBlockIsType(block, Cases.class); + Set caseIds = + caseSuccessor.getCaseSuccessors().stream() + .map(BlockChecker::getBlockId) + .collect(Collectors.toSet()); + assertThat(caseIds) + .withFailMessage( + getBlockDisplay(block) + + " is expected to have case successors of [" + + expectedCases.stream() + .map(id -> "B" + id) + .collect(Collectors.joining(", ")) + + "]") + .containsExactlyInAnyOrderElementsOf(expectedCases); + assertThat(getBlockId(caseSuccessor.getFallthroughSuccessor())) + .withFailMessage( + getBlockDisplay(block) + + " is expected to have fallthrough successor of B" + + fallthrough) + .isEqualTo(fallthrough); + }); + return this; + } + + public BlockChecker succeedsToWithExceptions(int successor, int... unknownExceptions) { + Set exceptions = Arrays.stream(unknownExceptions).boxed().collect(Collectors.toSet()); + this.successorChecker = + new BlockDetailChecker( + block -> { + UnknownException branch = assertBlockIsType(block, UnknownException.class); + assertThat(getBlockId(branch.getSuccessor())) + .withFailMessage( + getBlockDisplay(block) + " is expected to have successor of B" + successor) + .isEqualTo(successor); + Set blockIds = + branch.getExceptions().stream() + .map(BlockChecker::getBlockId) + .collect(Collectors.toSet()); + assertThat(blockIds) + .withFailMessage( + getBlockDisplay(block) + + " is expected to have exception successors of [" + + exceptions.stream() + .map(id -> "B" + id) + .collect(Collectors.joining(", ")) + + "]") + .containsExactlyInAnyOrderElementsOf(exceptions); + }); + return this; + } + + public BlockChecker isSink() { + successorChecker = new BlockDetailChecker(block -> assertBlockIsType(block, Halt.class)); + return this; + } + + private T assertBlockIsType(Block block, Class type) { + assertThat(block).as("block type").isInstanceOf(type); + return type.cast(block); + } + + public BlockChecker withTerminator(Class terminatorClass) { + return withTerminator(terminatorClass, TerminatorKind.NODE); + } + + public BlockChecker withTerminator(StatementTerminator terminator) { + this.terminatorChecker = + new BlockDetailChecker( + block -> { + Terminated terminated = assertBlockTerminated(block); + assertThat(terminated.getTerminatorKind()) + .withFailMessage( + getBlockDisplay(block) + + " is expected to be terminated with kind " + + terminator.getTerminatorKind()) + .isEqualTo(terminator.getTerminatorKind()); + + assertThat(terminated.getTerminator()) + .withFailMessage( + getBlockDisplay(block) + " is expected to be terminated with name reference") + .isInstanceOf(NameReferenceNode.class); + NameReferenceNode nameReferenceNode = (NameReferenceNode) terminated.getTerminator(); + assertThat(nameReferenceNode.getLastName().getNameDeclaration()) + .withFailMessage( + getBlockDisplay(block) + " is expected to be terminated with routine") + .isInstanceOf(RoutineNameDeclaration.class); + assertThat( + ((RoutineNameDeclaration) + nameReferenceNode.getLastName().getNameDeclaration()) + .fullyQualifiedName()) + .withFailMessage( + getBlockDisplay(block) + + " is expected to be terminated with " + + terminator.getRoutineName()) + .isEqualTo(terminator.getRoutineName()); + }); + return this; + } + + public BlockChecker withTerminator( + Class terminatorClass, TerminatorKind kind) { + this.terminatorChecker = + new BlockDetailChecker( + block -> { + Terminated terminated = assertBlockTerminated(block); + assertThat(terminated.getTerminator()) + .withFailMessage( + getBlockDisplay(block) + + " is expected to be terminated with " + + terminatorClass.getTypeName()) + .isInstanceOf(terminatorClass); + assertThat(terminated.getTerminatorKind()) + .withFailMessage( + getBlockDisplay(block) + " is expected to be terminated with kind " + kind) + .isEqualTo(kind); + }); + return this; + } + + public BlockChecker withTerminatorNodeCheck(Consumer extraChecker) { + this.terminatorNodeChecks.add( + new BlockDetailChecker( + block -> { + Terminated terminated = assertBlockTerminated(block); + extraChecker.accept(terminated.getTerminator()); + })); + return this; + } + + private Terminated assertBlockTerminated(Block block) { + assertThat(block) + .withFailMessage(getBlockDisplay(block) + " is expected to be terminated") + .isInstanceOf(Terminated.class); + return (Terminated) block; + } + + private String getBlockDisplay(Block block) { + return "B" + getBlockId(block); + } + + private static int getBlockId(Block block) { + return ((BlockImpl) block).getId(); + } +} + +class BlockDetailChecker { + private final Consumer toCheck; + + public BlockDetailChecker(Consumer toCheck) { + this.toCheck = toCheck; + } + + public void check(final Block block) { + this.toCheck.accept(block); + } +} diff --git a/delphi-frontend/src/test/java/au/com/integradev/delphi/cfg/checker/ElementChecker.java b/delphi-frontend/src/test/java/au/com/integradev/delphi/cfg/checker/ElementChecker.java new file mode 100644 index 000000000..57b741ef8 --- /dev/null +++ b/delphi-frontend/src/test/java/au/com/integradev/delphi/cfg/checker/ElementChecker.java @@ -0,0 +1,85 @@ +/* + * Sonar Delphi Plugin + * Copyright (C) 2025 Integrated Application Development + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package au.com.integradev.delphi.cfg.checker; + +import static org.assertj.core.api.Assertions.*; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import org.sonar.plugins.communitydelphi.api.ast.DelphiNode; + +public class ElementChecker { + private int blockId; + private int elementId; + private final List> checkers = new ArrayList<>(); + + public static ElementChecker element(Class elementClass) { + return new ElementChecker( + (elementChecker, node) -> assertElementIsType(elementChecker, elementClass, node)); + } + + public static ElementChecker element(Class elementClass, String image) { + return new ElementChecker( + (elementChecker, node) -> { + assertElementIsType(elementChecker, elementClass, node); + assertThat(node.getImage()) + .as(elementChecker.getElementId() + " is expected to have image " + image) + .isEqualTo(image); + }); + } + + private static void assertElementIsType( + ElementChecker elementChecker, Class elementClass, DelphiNode node) { + assertThat(node) + .as( + elementChecker.getElementId() + + " is expected to be of type " + + elementClass.getTypeName()) + .isInstanceOf(elementClass); + } + + private String getElementId() { + return "B" + blockId + ":E" + elementId; + } + + private ElementChecker(BiConsumer checker) { + this.checkers.add(checker); + } + + public ElementChecker withCheck(Consumer checker) { + return withCheck((elementChecker, node) -> checker.accept(node)); + } + + public ElementChecker withCheck(BiConsumer checker) { + this.checkers.add(checker); + return this; + } + + protected ElementChecker withBlockId(int blockId, int elementId) { + this.blockId = blockId; + this.elementId = elementId; + return this; + } + + public void check(final DelphiNode element) { + this.checkers.forEach(checker -> checker.accept(this, element)); + } +} diff --git a/delphi-frontend/src/test/java/au/com/integradev/delphi/cfg/checker/GraphChecker.java b/delphi-frontend/src/test/java/au/com/integradev/delphi/cfg/checker/GraphChecker.java new file mode 100644 index 000000000..b33b303b8 --- /dev/null +++ b/delphi-frontend/src/test/java/au/com/integradev/delphi/cfg/checker/GraphChecker.java @@ -0,0 +1,70 @@ +/* + * Sonar Delphi Plugin + * Copyright (C) 2025 Integrated Application Development + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package au.com.integradev.delphi.cfg.checker; + +import static org.assertj.core.api.Assertions.*; + +import au.com.integradev.delphi.cfg.api.Block; +import au.com.integradev.delphi.cfg.api.ControlFlowGraph; +import au.com.integradev.delphi.cfg.block.BlockImpl; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +public class GraphChecker { + private final List checkers = new ArrayList<>(); + + public static GraphChecker checker(BlockChecker... blocks) { + return new GraphChecker(blocks); + } + + private GraphChecker(BlockChecker... checkers) { + Collections.addAll(this.checkers, checkers); + } + + public void check(final ControlFlowGraph cfg) { + assertThat(cfg.getBlocks()).as("block count").hasSize(checkers.size() + 1); + final Iterator checkerIterator = checkers.iterator(); + + List blocks = new ArrayList<>(cfg.getBlocks()); + final Block exitBlock = blocks.remove(blocks.size() - 1); + for (Block block : blocks) { + checkerIterator.next().check(block); + int blockId = ((BlockImpl) block).getId(); + checkLinkedBlocks("Successor of B" + blockId, cfg.getBlocks(), block.getSuccessors()); + checkLinkedBlocks("Predecessor of B" + blockId, cfg.getBlocks(), block.getPredecessors()); + } + assertThat(exitBlock.getElements()).isEmpty(); + assertThat(exitBlock.getSuccessors()).isEmpty(); + assertThat(cfg.getBlocks()) + .withFailMessage("CFG entry block is no longer in the list of blocks!") + .contains(cfg.getEntryBlock()); + } + + private void checkLinkedBlocks(String type, List blocks, Set linkedBlocks) { + for (Block block : linkedBlocks) { + assertThat(block) + .withFailMessage( + type + ", block B" + ((BlockImpl) block).getId() + " is missing from CFG's blocks") + .isIn(blocks); + } + } +} diff --git a/delphi-frontend/src/test/java/au/com/integradev/delphi/cfg/checker/StatementTerminator.java b/delphi-frontend/src/test/java/au/com/integradev/delphi/cfg/checker/StatementTerminator.java new file mode 100644 index 000000000..eb972ca77 --- /dev/null +++ b/delphi-frontend/src/test/java/au/com/integradev/delphi/cfg/checker/StatementTerminator.java @@ -0,0 +1,44 @@ +/* + * Sonar Delphi Plugin + * Copyright (C) 2025 Integrated Application Development + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package au.com.integradev.delphi.cfg.checker; + +import au.com.integradev.delphi.cfg.block.TerminatorKind; + +public enum StatementTerminator { + EXIT("System.Exit", TerminatorKind.EXIT), + BREAK("System.Break", TerminatorKind.BREAK), + HALT("System.Halt", TerminatorKind.HALT), + CONTINUE("System.Continue", TerminatorKind.CONTINUE); + + private final String routineName; + private final TerminatorKind terminatorKind; + + StatementTerminator(String routineName, TerminatorKind terminatorKind) { + this.routineName = routineName; + this.terminatorKind = terminatorKind; + } + + public String getRoutineName() { + return routineName; + } + + public TerminatorKind getTerminatorKind() { + return terminatorKind; + } +}