From 5477c50128d79c9e285ba0550f80a8f80e3a39f2 Mon Sep 17 00:00:00 2001 From: Elliot Hillary Date: Fri, 25 Oct 2024 15:18:20 +1100 Subject: [PATCH 1/2] Add check for unraised exceptions --- CHANGELOG.md | 1 + .../integradev/delphi/checks/CheckList.java | 1 + .../delphi/checks/MissingRaiseCheck.java | 84 ++++++++ .../rules/community-delphi/MissingRaise.html | 32 +++ .../rules/community-delphi/MissingRaise.json | 21 ++ .../community-delphi/Sonar_way_profile.json | 1 + .../delphi/checks/MissingRaiseCheckTest.java | 182 ++++++++++++++++++ 7 files changed, 322 insertions(+) create mode 100644 delphi-checks/src/main/java/au/com/integradev/delphi/checks/MissingRaiseCheck.java create mode 100644 delphi-checks/src/main/resources/org/sonar/l10n/delphi/rules/community-delphi/MissingRaise.html create mode 100644 delphi-checks/src/main/resources/org/sonar/l10n/delphi/rules/community-delphi/MissingRaise.json create mode 100644 delphi-checks/src/test/java/au/com/integradev/delphi/checks/MissingRaiseCheckTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index fd0ff9f41..03304d806 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- `MissingRaise` analysis rule, which flags exception constructions for which `raise` has been omitted. - **API:** `ProceduralType::directives` method. - **API:** `ProceduralTypeNode::getDirectives` method. - **API:** `ProceduralTypeNode::hasDirective` 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 720fee091..1d9de99ac 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 @@ -107,6 +107,7 @@ public final class CheckList { LowercaseKeywordCheck.class, MathFunctionSingleOverloadCheck.class, MemberDeclarationOrderCheck.class, + MissingRaiseCheck.class, MissingSemicolonCheck.class, MixedNamesCheck.class, NilComparisonCheck.class, diff --git a/delphi-checks/src/main/java/au/com/integradev/delphi/checks/MissingRaiseCheck.java b/delphi-checks/src/main/java/au/com/integradev/delphi/checks/MissingRaiseCheck.java new file mode 100644 index 000000000..46c5ba3ac --- /dev/null +++ b/delphi-checks/src/main/java/au/com/integradev/delphi/checks/MissingRaiseCheck.java @@ -0,0 +1,84 @@ +/* + * 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 org.sonar.check.Rule; +import org.sonar.plugins.communitydelphi.api.ast.ExpressionNode; +import org.sonar.plugins.communitydelphi.api.ast.ExpressionStatementNode; +import org.sonar.plugins.communitydelphi.api.ast.NameReferenceNode; +import org.sonar.plugins.communitydelphi.api.ast.PrimaryExpressionNode; +import org.sonar.plugins.communitydelphi.api.check.DelphiCheck; +import org.sonar.plugins.communitydelphi.api.check.DelphiCheckContext; +import org.sonar.plugins.communitydelphi.api.reporting.QuickFix; +import org.sonar.plugins.communitydelphi.api.reporting.QuickFixEdit; +import org.sonar.plugins.communitydelphi.api.symbol.declaration.NameDeclaration; +import org.sonar.plugins.communitydelphi.api.symbol.declaration.RoutineKind; +import org.sonar.plugins.communitydelphi.api.symbol.declaration.RoutineNameDeclaration; +import org.sonar.plugins.communitydelphi.api.symbol.declaration.TypeNameDeclaration; +import org.sonar.plugins.communitydelphi.api.type.Type; + +@Rule(key = "MissingRaise") +public class MissingRaiseCheck extends DelphiCheck { + private static final String BASE_EXCEPTION = "System.SysUtils.Exception"; + + @Override + public DelphiCheckContext visit( + ExpressionStatementNode expressionStatement, DelphiCheckContext context) { + if (isExceptionConstructorInvocation(expressionStatement.getExpression())) { + context + .newIssue() + .onNode(expressionStatement) + .withMessage("Raise or delete this exception.") + .withQuickFixes( + QuickFix.newFix("Raise exception") + .withEdit( + QuickFixEdit.insertBefore("raise ", expressionStatement.getExpression()))) + .report(); + } + + return context; + } + + private boolean isExceptionConstructorInvocation(ExpressionNode expression) { + if (!(expression instanceof PrimaryExpressionNode) + || !(expression.getChild(0) instanceof NameReferenceNode)) { + return false; + } + + NameDeclaration declaration = + ((NameReferenceNode) expression.getChild(0)).getLastName().getNameDeclaration(); + if (!(declaration instanceof RoutineNameDeclaration)) { + return false; + } + + RoutineNameDeclaration routineDeclaration = (RoutineNameDeclaration) declaration; + if (routineDeclaration.getRoutineKind() != RoutineKind.CONSTRUCTOR) { + return false; + } + + TypeNameDeclaration typeDecl = routineDeclaration.getTypeDeclaration(); + if (typeDecl == null) { + return false; + } + + Type type = typeDecl.getType(); + + return type.is(BASE_EXCEPTION) || type.isDescendantOf(BASE_EXCEPTION); + } +} diff --git a/delphi-checks/src/main/resources/org/sonar/l10n/delphi/rules/community-delphi/MissingRaise.html b/delphi-checks/src/main/resources/org/sonar/l10n/delphi/rules/community-delphi/MissingRaise.html new file mode 100644 index 000000000..538413156 --- /dev/null +++ b/delphi-checks/src/main/resources/org/sonar/l10n/delphi/rules/community-delphi/MissingRaise.html @@ -0,0 +1,32 @@ +

Why is this an issue?

+

+ When writing an exception raise in Delphi, a common mistake is to omit the raise, + causing the exception to be created but not actually used. This is bad for a number of reasons: +

+ +

How to fix it

+

Add the raise keyword. If the exception is not required, delete the constructor invocation instead.

+
+procedure DeleteDatabase;
+begin
+  if InProductionEnvironment then begin
+    EDontBreakProduction.Create('DeleteDatabase attempted in production');
+  end;
+
+  Database.DeleteAllImportantRecords;
+end;
+
+
+procedure DeleteDatabase;
+begin
+  if InProductionEnvironment then begin
+    raise EDontBreakProduction.Create('DeleteDatabase attempted in production');
+  end;
+
+  Database.DeleteAllImportantRecords;
+end;
+
\ No newline at end of file diff --git a/delphi-checks/src/main/resources/org/sonar/l10n/delphi/rules/community-delphi/MissingRaise.json b/delphi-checks/src/main/resources/org/sonar/l10n/delphi/rules/community-delphi/MissingRaise.json new file mode 100644 index 000000000..7513788a8 --- /dev/null +++ b/delphi-checks/src/main/resources/org/sonar/l10n/delphi/rules/community-delphi/MissingRaise.json @@ -0,0 +1,21 @@ +{ + "title": "Exceptions should be raised", + "type": "BUG", + "status": "ready", + "remediation": { + "func": "Constant/Issue", + "constantCost": "1min" + }, + "code": { + "attribute": "COMPLETE", + "impacts": { + "RELIABILITY": "HIGH" + } + }, + "tags": [ + "suspicious" + ], + "defaultSeverity": "Critical", + "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 5a35d3911..6c31c4c6a 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 @@ -52,6 +52,7 @@ "LowercaseKeyword", "MathFunctionSingleOverload", "MemberDeclarationOrder", + "MissingRaise", "MissingSemicolon", "MixedNames", "NilComparison", diff --git a/delphi-checks/src/test/java/au/com/integradev/delphi/checks/MissingRaiseCheckTest.java b/delphi-checks/src/test/java/au/com/integradev/delphi/checks/MissingRaiseCheckTest.java new file mode 100644 index 000000000..61544be94 --- /dev/null +++ b/delphi-checks/src/test/java/au/com/integradev/delphi/checks/MissingRaiseCheckTest.java @@ -0,0 +1,182 @@ +/* + * 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 au.com.integradev.delphi.builders.DelphiTestUnitBuilder; +import au.com.integradev.delphi.checks.verifier.CheckVerifier; +import org.junit.jupiter.api.Test; + +class MissingRaiseCheckTest { + private DelphiTestUnitBuilder sysUtils() { + return new DelphiTestUnitBuilder() + .unitName("System.SysUtils") + .appendDecl("type") + .appendDecl(" Exception = class(TObject)") + .appendDecl(" public") + .appendDecl(" constructor Create(Text: string);") + .appendDecl(" end;"); + } + + @Test + void testDiscardedBaseExceptionShouldAddIssue() { + CheckVerifier.newVerifier() + .withCheck(new MissingRaiseCheck()) + .withStandardLibraryUnit(sysUtils()) + .onFile( + new DelphiTestUnitBuilder() + .appendImpl("uses System.SysUtils;") + .appendImpl("initialization") + .appendImpl(" Exception.Create('Error!'); // Noncompliant")) + .verifyIssues(); + } + + @Test + void testDiscardedDescendantExceptionShouldAddIssue() { + CheckVerifier.newVerifier() + .withCheck(new MissingRaiseCheck()) + .withStandardLibraryUnit(sysUtils()) + .onFile( + new DelphiTestUnitBuilder() + .appendDecl("uses System.SysUtils;") + .appendDecl("type") + .appendDecl(" ECalculatorError = class(Exception)") + .appendDecl(" public") + .appendDecl(" constructor Create(A: Integer; B: Integer);") + .appendDecl(" end;") + .appendImpl("initialization") + .appendImpl(" ECalculatorError.Create(1, 2); // Noncompliant")) + .verifyIssues(); + } + + @Test + void testDiscardedNonExceptionShouldNotAddIssue() { + CheckVerifier.newVerifier() + .withCheck(new MissingRaiseCheck()) + .withStandardLibraryUnit(sysUtils()) + .onFile( + new DelphiTestUnitBuilder() + .appendDecl("uses System.SysUtils;") + .appendDecl("type") + .appendDecl(" EConfusinglyNamedNormalObject = class(TObject)") + .appendDecl(" public") + .appendDecl(" constructor Create(A: Integer; B: Integer);") + .appendDecl(" end;") + .appendImpl("initialization") + .appendImpl(" EConfusinglyNamedNormalObject.Create(1, 2);")) + .verifyNoIssues(); + } + + @Test + void testRaisedBaseExceptionShouldNotAddIssue() { + CheckVerifier.newVerifier() + .withCheck(new MissingRaiseCheck()) + .withStandardLibraryUnit(sysUtils()) + .onFile( + new DelphiTestUnitBuilder() + .appendImpl("uses System.SysUtils;") + .appendImpl("initialization") + .appendImpl(" raise Exception.Create('Error!');")) + .verifyNoIssues(); + } + + @Test + void testDiscardedInvocationShouldNotAddIssue() { + CheckVerifier.newVerifier() + .withCheck(new MissingRaiseCheck()) + .withStandardLibraryUnit(sysUtils()) + .onFile( + new DelphiTestUnitBuilder() + .appendImpl("uses System.SysUtils;") + .appendImpl("function Foo: Exception;") + .appendImpl("begin") + .appendImpl("end;") + .appendImpl("initialization") + .appendImpl(" Foo;")) + .verifyNoIssues(); + } + + @Test + void testDiscardedNonConstructorInvocationShouldNotAddIssue() { + CheckVerifier.newVerifier() + .withCheck(new MissingRaiseCheck()) + .withStandardLibraryUnit(sysUtils()) + .onFile( + new DelphiTestUnitBuilder() + .appendDecl("uses System.SysUtils;") + .appendDecl("type") + .appendDecl(" ECalculatorError = class(Exception)") + .appendDecl(" public") + .appendDecl(" class function Add(A: Integer; B: Integer);") + .appendDecl(" end;") + .appendImpl("initialization") + .appendImpl(" ECalculatorError.Add(1, 2);")) + .verifyNoIssues(); + } + + @Test + void testRaisedDescendantExceptionShouldNotAddIssue() { + CheckVerifier.newVerifier() + .withCheck(new MissingRaiseCheck()) + .withStandardLibraryUnit(sysUtils()) + .onFile( + new DelphiTestUnitBuilder() + .appendDecl("uses System.SysUtils;") + .appendDecl("type") + .appendDecl(" ECalculatorError = class(Exception)") + .appendDecl(" public") + .appendDecl(" constructor Create(A: Integer; B: Integer);") + .appendDecl(" end;") + .appendImpl("initialization") + .appendImpl(" raise ECalculatorError.Create(1, 2);")) + .verifyNoIssues(); + } + + @Test + void testAssignedBaseExceptionShouldNotAddIssue() { + CheckVerifier.newVerifier() + .withCheck(new MissingRaiseCheck()) + .withStandardLibraryUnit(sysUtils()) + .onFile( + new DelphiTestUnitBuilder() + .appendImpl("uses System.SysUtils;") + .appendImpl("var MyError: ECalculatorError;") + .appendImpl("initialization") + .appendImpl(" MyError := Exception.Create('Error!');")) + .verifyNoIssues(); + } + + @Test + void testAssignedDescendantExceptionShouldNotAddIssue() { + CheckVerifier.newVerifier() + .withCheck(new MissingRaiseCheck()) + .withStandardLibraryUnit(sysUtils()) + .onFile( + new DelphiTestUnitBuilder() + .appendDecl("uses System.SysUtils;") + .appendDecl("type") + .appendDecl(" ECalculatorError = class(Exception)") + .appendDecl(" public") + .appendDecl(" constructor Create(A: Integer; B: Integer);") + .appendDecl(" end;") + .appendImpl("var MyError: ECalculatorError;") + .appendImpl("initialization") + .appendImpl(" MyError := ECalculatorError.Create(1, 2);")) + .verifyNoIssues(); + } +} From dc712dacb7171dc0d36ef1a01090046cca9ea53c Mon Sep 17 00:00:00 2001 From: Elliot Hillary Date: Fri, 25 Oct 2024 15:20:43 +1100 Subject: [PATCH 2/2] Fix changelog rule typo --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 03304d806..67318357a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,7 +32,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- `FullyQualifiedImportCheck` analysis rule, which flags non-fully qualified imports. +- `FullyQualifiedImport` analysis rule, which flags non-fully qualified imports. - **API:** `UnitImportNameDeclaration::isAlias` method. ### Changed