From 434a17d72d0fff56972859d742fa3b765526f978 Mon Sep 17 00:00:00 2001 From: Joshua Gardner Date: Wed, 7 Aug 2024 11:13:55 +1000 Subject: [PATCH] Add check for loops executing at most once --- CHANGELOG.md | 1 + .../integradev/delphi/checks/CheckList.java | 1 + .../checks/LoopExecutingAtMostOnceCheck.java | 254 ++++++++++++++++ .../LoopExecutingAtMostOnce.html | 62 ++++ .../LoopExecutingAtMostOnce.json | 19 ++ .../LoopExecutingAtMostOnceCheckTest.java | 279 ++++++++++++++++++ 6 files changed, 616 insertions(+) create mode 100644 delphi-checks/src/main/java/au/com/integradev/delphi/checks/LoopExecutingAtMostOnceCheck.java create mode 100644 delphi-checks/src/main/resources/org/sonar/l10n/delphi/rules/community-delphi/LoopExecutingAtMostOnce.html create mode 100644 delphi-checks/src/main/resources/org/sonar/l10n/delphi/rules/community-delphi/LoopExecutingAtMostOnce.json create mode 100644 delphi-checks/src/test/java/au/com/integradev/delphi/checks/LoopExecutingAtMostOnceCheckTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index dee8b00d5..8e9fca8bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - "Correct to (correct case)" quick fix for `LowercaseKeyword`. - Ability to create a control flow graph. - `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:** `PropertyNameDeclaration::getImplementedTypes` method. - **API:** `PropertyNode::getDefaultSpecifier` method. - **API:** `PropertyNode::getImplementsSpecifier` method. 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 a2bacc2a3..fb029ad76 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 @@ -103,6 +103,7 @@ public final class CheckList { InterfaceGuidCheck.class, InterfaceNameCheck.class, LegacyInitializationSectionCheck.class, + LoopExecutingAtMostOnceCheck.class, LowercaseKeywordCheck.class, MathFunctionSingleOverloadCheck.class, MemberDeclarationOrderCheck.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..9a98599e7 --- /dev/null +++ b/delphi-checks/src/main/java/au/com/integradev/delphi/checks/LoopExecutingAtMostOnceCheck.java @@ -0,0 +1,254 @@ +/* + * Sonar Delphi Plugin + * Copyright (C) 2019 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.ControlFlowGraphImpl; +import au.com.integradev.delphi.cfg.ControlFlowGraphImpl.Block; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.PriorityQueue; +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.DelphiNode; +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.NameReferenceNode; +import org.sonar.plugins.communitydelphi.api.ast.RaiseStatementNode; +import org.sonar.plugins.communitydelphi.api.ast.RepeatStatementNode; +import org.sonar.plugins.communitydelphi.api.ast.StatementNode; +import org.sonar.plugins.communitydelphi.api.ast.WhileStatementNode; +import org.sonar.plugins.communitydelphi.api.cfg.ControlFlowGraph; +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 = "LoopExecutingAtMostOnce") +public class LoopExecutingAtMostOnceCheck extends DelphiCheck { + private static final Set EXIT_METHODS = + Set.of("System.Exit", "System.Break", "System.Halt"); + + @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, fullyQualifiedName); + } + + private DelphiCheckContext visitExitingNode( + DelphiNode node, DelphiCheckContext context, String description) { + DelphiNode enclosingStatement = findEnclosingStatement(node); + DelphiNode enclosingLoop = findEnclosingLoop(node); + if (enclosingStatement == null || enclosingLoop == null) { + return context; + } + + if (isViolation(enclosingLoop, node) && violationInRelevantStatement(enclosingStatement)) { + reportIssue( + context, + node, + String.format("Remove this \"%s\" statement or make it conditional.", description)); + } + return context; + } + + private static boolean isLoopNode(DelphiNode node) { + return node instanceof RepeatStatementNode + || node instanceof ForStatementNode + || node instanceof WhileStatementNode; + } + + private static DelphiNode findEnclosingStatement(DelphiNode node) { + DelphiNode parent = node; + if (!(parent instanceof StatementNode)) { + // Finding the enclosing statement of the NameReferenceNode + parent = parent.getFirstParentOfType(StatementNode.class); + } + if (parent == null) { + return null; + } + do { + parent = parent.getFirstParentOfType(StatementNode.class); + } while (parent instanceof CompoundStatementNode); + return parent; + } + + private static DelphiNode findEnclosingLoop(DelphiNode node) { + DelphiNode parent = node; + while (parent != null && !isLoopNode(parent)) { + parent = parent.getFirstParentOfType(StatementNode.class); + } + return parent; + } + + private static boolean isViolation(DelphiNode loop, DelphiNode jump) { + ControlFlowGraphImpl 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 violationInRelevantStatement(DelphiNode enclosingStatement) { + if (!(enclosingStatement instanceof IfStatementNode)) { + return true; + } + + IfStatementNode ifStatement = (IfStatementNode) enclosingStatement; + if (!ifStatement.hasElseBranch()) { + return false; + } + return !(ifStatement.getElseStatement() instanceof IfStatementNode); + } + + private static Optional getTerminatorBlock(ControlFlowGraphImpl cfg, DelphiNode loop) { + return cfg.blocks().stream().filter(block -> loop.equals(block.terminator())).findFirst(); + } + + private static boolean hasPredecessorInBlock(Block block, DelphiNode loop) { + for (Block predecessor : block.predecessors()) { + List predecessorElements = predecessor.elements(); + 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( + ControlFlowGraphImpl cfg, Block loopBlock, DelphiNode jump) { + if (!(jump instanceof GotoStatementNode)) { + return false; + } + Optional jumpBlock = getTerminatorBlock(cfg, jump); + if (jumpBlock.isEmpty()) { + return false; + } + Block jumpTarget = jumpBlock.get().successors().iterator().next(); + if (jumpTarget == null) { + return false; + } + if (loopBlock.terminator() instanceof RepeatStatementNode + && loopBlock.falseBlock().equals(jumpTarget)) { + return true; + } + + Set visited = new HashSet<>(); + PriorityQueue queue = + new PriorityQueue<>((a, b) -> Math.abs(a.id() - b.id() - loopBlock.id())); + queue.add(jumpTarget); + while (!queue.isEmpty()) { + Block search = queue.poll(); + if (search.equals(loopBlock)) { + return true; + } + if (search.successors().size() == 1 && search.successors().contains(jumpTarget)) { + return false; + } + + if (search.equals(cfg.exitBlock())) { + return false; + } + visited.add(search); + search.successors().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)::cfg; + } + if (node instanceof AnonymousMethodNodeImpl) { + return ((AnonymousMethodNodeImpl) node)::cfg; + } + return null; + } + + private static ControlFlowGraphImpl 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 (ControlFlowGraphImpl) cfgSupplier.get(); + } + return new ControlFlowGraphImpl(loop.findChildrenOfType(StatementNode.class)); + } +} 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..cb37210bd --- /dev/null +++ b/delphi-checks/src/main/resources/org/sonar/l10n/delphi/rules/community-delphi/LoopExecutingAtMostOnce.html @@ -0,0 +1,62 @@ +

Why is this an issue?

+

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

+

+ If the intention was to execute the loop once, an if statement should be used. + Otherwise, the jumping statement should be made conditional such that loop can execute multiple + times. +

+

+ 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
  • +
  • Continue
  • +
  • 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);
+    return; // 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
+    Writeln(I);
+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..62ede31df --- /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": "BUG", + "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/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..c6457455a --- /dev/null +++ b/delphi-checks/src/test/java/au/com/integradev/delphi/checks/LoopExecutingAtMostOnceCheckTest.java @@ -0,0 +1,279 @@ +/* + * Sonar Delphi Plugin + * Copyright (C) 2024 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.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +class LoopExecutingAtMostOnceCheckTest { + + enum LoopType { + While("while A do begin", "end;"), + ForIn("for var A in B do begin", "end;"), + ForTo("for var A := B to C do begin", "end;"), + ForDownTo("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; + } + } + + enum Issues { + ExpectingIssues, + ExpectingNoIssues + } + + private void doLoopTest(LoopType loopType, List loopContents, Issues issues) { + 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 LoopExecutingAtMostOnceCheck()) + .onFile(unitBuilder); + if (issues == Issues.ExpectingIssues) { + verifier.verifyIssues(); + } else { + verifier.verifyNoIssues(); + } + } + + private void doRoutineTest(List functionContents, Issues issues) { + 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("label before, middle, after;") + .appendImpl("begin"); + for (String loopLine : functionContents) { + unitBuilder.appendImpl(" " + loopLine); + } + unitBuilder.appendImpl("end;"); + + var verifier = + CheckVerifier.newVerifier() + .withCheck(new LoopExecutingAtMostOnceCheck()) + .onFile(unitBuilder); + if (issues == Issues.ExpectingIssues) { + verifier.verifyIssues(); + } else { + verifier.verifyNoIssues(); + } + } + + // Continue + @ParameterizedTest + @EnumSource(value = LoopType.class) + void testUnconditionalContinueShouldNotAddIssue(LoopType loopType) { + doLoopTest(loopType, List.of("Continue; // Compliant"), Issues.ExpectingNoIssues); + } + + // Break + @ParameterizedTest + @EnumSource(value = LoopType.class) + void testUnconditionalBreakShouldAddIssue(LoopType loopType) { + doLoopTest(loopType, List.of("Break; // Noncompliant"), Issues.ExpectingIssues); + } + + @ParameterizedTest + @EnumSource(value = LoopType.class) + void testConditionalBreakShouldNotAddIssue(LoopType loopType) { + doLoopTest(loopType, List.of("if A then Break; // Compliant"), Issues.ExpectingNoIssues); + } + + // Exit + @ParameterizedTest + @EnumSource(value = LoopType.class) + void testUnconditionalExitShouldAddIssue(LoopType loopType) { + doLoopTest(loopType, List.of("Exit; // Noncompliant"), Issues.ExpectingIssues); + } + + @ParameterizedTest + @EnumSource(value = LoopType.class) + void testConditionalExitShouldNotAddIssue(LoopType loopType) { + doLoopTest(loopType, List.of("if A then Exit; // Compliant"), Issues.ExpectingNoIssues); + } + + // Halt + @ParameterizedTest + @EnumSource(value = LoopType.class) + void testUnconditionalHaltShouldAddIssue(LoopType loopType) { + doLoopTest(loopType, List.of("Halt; // Noncompliant"), Issues.ExpectingIssues); + } + + @ParameterizedTest + @EnumSource(value = LoopType.class) + void testConditionalHaltShouldNotAddIssue(LoopType loopType) { + doLoopTest(loopType, List.of("if A then Halt; // Compliant"), Issues.ExpectingNoIssues); + } + + // Raise + @ParameterizedTest + @EnumSource(value = LoopType.class) + void testUnconditionalRaiseShouldAddIssue(LoopType loopType) { + doLoopTest(loopType, List.of("raise A; // Noncompliant"), Issues.ExpectingIssues); + } + + @ParameterizedTest + @EnumSource(value = LoopType.class) + void testConditionalRaiseShouldNotAddIssue(LoopType loopType) { + doLoopTest(loopType, List.of("if A then raise B; // Compliant"), Issues.ExpectingNoIssues); + } + + // Goto + @ParameterizedTest + @EnumSource(value = LoopType.class) + void testUnconditionalGotoBeforeShouldNotAddIssue(LoopType loopType) { + doRoutineTest( + List.of("before:", loopType.loopHeader, " goto before; // Compliant", loopType.loopFooter), + Issues.ExpectingNoIssues); + } + + @ParameterizedTest + @EnumSource(value = LoopType.class) + void testUnconditionalGotoAfterShouldNotAddIssue(LoopType loopType) { + doRoutineTest( + List.of( + loopType.loopHeader, " goto after; // Noncompliant", loopType.loopFooter, "after:"), + Issues.ExpectingIssues); + } + + @ParameterizedTest + @EnumSource(value = LoopType.class) + void testConditionalGotoShouldNotAddIssue(LoopType loopType) { + doRoutineTest( + List.of( + "before:", + loopType.loopHeader, + " if A then goto before; // Compliant", + loopType.loopFooter), + Issues.ExpectingNoIssues); + } + + @ParameterizedTest + @EnumSource(value = LoopType.class) + void testGotoBeforeExitShouldAddIssue(LoopType loopType) { + doRoutineTest( + List.of( + "before:", + "Exit;", + loopType.loopHeader, + " goto before; // Noncompliant", + loopType.loopFooter), + Issues.ExpectingIssues); + } + + @ParameterizedTest + @EnumSource(value = LoopType.class) + void testGotoMultiBlockInfiniteLoopShouldAddIssue(LoopType loopType) { + doRoutineTest( + List.of( + "before:", + "Writeln('A');", + "middle:", + "goto before;", + loopType.loopHeader, + " goto middle; // Noncompliant", + loopType.loopFooter), + Issues.ExpectingIssues); + } + + @ParameterizedTest + @EnumSource(value = LoopType.class) + void testGotoSameBlockInfiniteLoopShouldAddIssue(LoopType loopType) { + doRoutineTest( + List.of( + "before:", + "goto before;", + loopType.loopHeader, + " goto before; // Noncompliant", + loopType.loopFooter), + Issues.ExpectingIssues); + } + + // Mixed + @ParameterizedTest + @EnumSource(value = LoopType.class) + void testIfBreakElseExitShouldAddIssue(LoopType loopType) { + doLoopTest( + loopType, + List.of("if A then", " Break // Noncompliant", "else", " Exit; // Noncompliant"), + Issues.ExpectingIssues); + } + + @ParameterizedTest + @EnumSource(value = LoopType.class) + void testIfBreakElseIfExitShouldNotAddIssue(LoopType loopType) { + doLoopTest( + loopType, + List.of("if A then", " Break // Compliant", "else if B then", " Exit; // Compliant"), + Issues.ExpectingNoIssues); + } + + @ParameterizedTest + @EnumSource(value = LoopType.class) + void testIfExitElseIfBreakThenExitShouldAddIssue(LoopType loopType) { + doLoopTest( + loopType, + List.of( + "if A then", + " Exit // Compliant", + "else if B then", + " Break; // Compliant", + "Exit // Noncompliant"), + Issues.ExpectingIssues); + } + + @ParameterizedTest + @EnumSource(value = LoopType.class) + void testIfContinueElseExitShouldNotAddIssue(LoopType loopType) { + doLoopTest( + loopType, + List.of("if A then", " Continue", "else", " Exit; // Compliant"), + Issues.ExpectingNoIssues); + } + + @ParameterizedTest + @EnumSource(value = LoopType.class) + void testConditionalBreakAndUnconditionalExitShouldAddIssue(LoopType loopType) { + doLoopTest( + loopType, + List.of("if B then", " Break; // Compliant", "Exit; // Noncompliant"), + Issues.ExpectingIssues); + } +}