diff --git a/.claude/skills/new-rule/SKILL.md b/.claude/skills/new-rule/SKILL.md index 39e782a3b30..66d5e9edaae 100644 --- a/.claude/skills/new-rule/SKILL.md +++ b/.claude/skills/new-rule/SKILL.md @@ -14,11 +14,12 @@ This skill provides sonar-java-specific guidelines for implementing new rules. - Ensure your local rspec repository is up-to-date with the rule branch - Use rule-api jar (check Maven local repository for available versions) - Command: `java -jar generate -branch rule/add-RSPEC-S{RULE_ID} -rule S{RULE_ID}` + - If the branch `rule/add-RSPEC-S{RULE_ID}` does not exist (e.g., for older rules already merged to master), fall back to: `java -jar generate -rule S{RULE_ID}` - This generates HTML and JSON files and updates the Sonar way profile automatically - Generated files will be placed in: - `sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S{RULE_ID}.html` - `sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S{RULE_ID}.json` - - `sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/Sonar_way_profile.json` (updated) + - `sonar-java-plugin/src/main/resources/profiles` ### 2. Tests - Run JavaAgenticWayProfileTest before creating a PR diff --git a/java-checks-test-sources/default/src/main/java/checks/InappropriateCastCheckSample.java b/java-checks-test-sources/default/src/main/java/checks/InappropriateCastCheckSample.java new file mode 100644 index 00000000000..d45d9175cff --- /dev/null +++ b/java-checks-test-sources/default/src/main/java/checks/InappropriateCastCheckSample.java @@ -0,0 +1,113 @@ +package checks; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +public class InappropriateCastCheckSample { + + interface Animal {} + interface Vehicle {} + interface Drawable {} + + static class Dog implements Animal {} + static final class FinalDog implements Animal {} + static class Car implements Vehicle {} + static class Circle extends Shape implements Drawable {} + static class Shape {} + static abstract class AbstractShape {} + + enum Color { RED, GREEN, BLUE } + enum Size { SMALL, MEDIUM, LARGE } + + // Noncompliant: unrelated concrete classes + void unrelatedConcreteClasses(Dog dog, Car car, String str) { + Car c = (Car) dog; // Noncompliant {{"Dog" cannot be cast to "Car" without a risk of "ClassCastException".}} + Dog d = (Dog) car; // Noncompliant {{"Car" cannot be cast to "Dog" without a risk of "ClassCastException".}} + Dog d2 = (Dog) str; // Noncompliant {{"String" cannot be cast to "Dog" without a risk of "ClassCastException".}} + } + + // Noncompliant: final class to unrelated interface + void finalClassToUnrelatedInterface(FinalDog finalDog, String str) { + Vehicle v = (Vehicle) finalDog; // Noncompliant {{"FinalDog" cannot be cast to "Vehicle" without a risk of "ClassCastException".}} + Drawable d = (Drawable) str; // Noncompliant {{"String" cannot be cast to "Drawable" without a risk of "ClassCastException".}} + } + + // Noncompliant: interface to unrelated final class + void interfaceToUnrelatedFinalClass(Vehicle vehicle, Drawable drawable) { + FinalDog fd = (FinalDog) vehicle; // Noncompliant {{"Vehicle" cannot be cast to "FinalDog" without a risk of "ClassCastException".}} + String s = (String) drawable; // Noncompliant {{"Drawable" cannot be cast to "String" without a risk of "ClassCastException".}} + } + + // Noncompliant: enums are implicitly final + void enumCasts(Color color, Size size) { + Size s = (Size) color; // Noncompliant {{"Color" cannot be cast to "Size" without a risk of "ClassCastException".}} + Drawable d = (Drawable) color; // Noncompliant {{"Color" cannot be cast to "Drawable" without a risk of "ClassCastException".}} + } + + // Compliant: upcast (subtype to supertype) + void upcast(Circle circle, Dog dog) { + Shape s = (Shape) circle; // Compliant + Object o = (Object) dog; // Compliant + Animal a = (Animal) dog; // Compliant + } + + // Compliant: downcast along hierarchy + void downcast(Shape shape, Animal animal) { + Circle c = (Circle) shape; // Compliant + Dog d = (Dog) animal; // Compliant + } + + // Compliant: cast to/from Object + void objectCasts(Object obj, Dog dog) { + Dog d = (Dog) obj; // Compliant + Object o = (Object) dog; // Compliant + } + + // Compliant: cast between interfaces + void interfaceCasts(Animal animal, Vehicle vehicle) { + Vehicle v = (Vehicle) animal; // Compliant + Animal a = (Animal) vehicle; // Compliant + } + + // Compliant: non-final class to unrelated interface + void nonFinalClassToInterface(Dog dog, Shape shape) { + Vehicle v = (Vehicle) dog; // Compliant + Serializable s = (Serializable) shape; // Compliant + } + + // Compliant: cast involving generics/wildcards + void genericCasts(List wildcardList, List intList) { + List strList = (List) wildcardList; // Compliant + ArrayList al = (ArrayList) intList; // Compliant + } + + // Compliant: instanceof guard (still a valid downcast along hierarchy) + void instanceofGuard(Object obj) { + if (obj instanceof String) { + String s = (String) obj; // Compliant + } + } + + // Compliant: cast to related interface (class implements interface) + void relatedInterfaceCast(Circle circle) { + Drawable d = (Drawable) circle; // Compliant + } + + // Compliant: abstract class to interface + void abstractClassToInterface(AbstractShape abstractShape) { + Drawable d = (Drawable) abstractShape; // Compliant + } + + // Compliant: cast involving type variables + void typeVariableCast(T obj) { + String s = (String) obj; // Compliant + } + + // Compliant: primitive casts + void primitiveCast(int i) { + long l = (long) i; // Compliant + double d = (double) i; // Compliant + } + +} diff --git a/java-checks/src/main/java/org/sonar/java/checks/InappropriateCastCheck.java b/java-checks/src/main/java/org/sonar/java/checks/InappropriateCastCheck.java new file mode 100644 index 00000000000..c198ee3a842 --- /dev/null +++ b/java-checks/src/main/java/org/sonar/java/checks/InappropriateCastCheck.java @@ -0,0 +1,75 @@ +/* + * SonarQube Java + * Copyright (C) SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * You can redistribute and/or modify this program under the terms of + * the Sonar Source-Available License Version 1, as published by SonarSource Sàrl. + * + * 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 Sonar Source-Available License for more details. + * + * You should have received a copy of the Sonar Source-Available License + * along with this program; if not, see https://sonarsource.com/license/ssal/ + */ +package org.sonar.java.checks; + +import java.util.Collections; +import java.util.List; +import org.sonar.check.Rule; +import org.sonar.plugins.java.api.IssuableSubscriptionVisitor; +import org.sonar.plugins.java.api.semantic.Type; +import org.sonar.plugins.java.api.tree.Tree; +import org.sonar.plugins.java.api.tree.TypeCastTree; + +@Rule(key = "S1944") +public class InappropriateCastCheck extends IssuableSubscriptionVisitor { + + @Override + public List nodesToVisit() { + return Collections.singletonList(Tree.Kind.TYPE_CAST); + } + + @Override + public void visitNode(Tree tree) { + TypeCastTree castTree = (TypeCastTree) tree; + Type sourceType = castTree.expression().symbolType().erasure(); + Type targetType = castTree.type().symbolType().erasure(); + + if (shouldSkip(sourceType) || shouldSkip(targetType)) { + return; + } + + if (sourceType.isSubtypeOf(targetType) || targetType.isSubtypeOf(sourceType)) { + return; + } + + if (areNeitherInterfaces(sourceType, targetType) || areTypesFinalClassAndInterface(sourceType, targetType)) { + reportIssue(castTree.type(), + String.format("\"%s\" cannot be cast to \"%s\" without a risk of \"ClassCastException\".", + sourceType.name(), targetType.name())); + } + } + + private static boolean shouldSkip(Type type) { + return type.isUnknown() + || type.isPrimitive() + || type.isVoid() + || type.isNullType() + || type.isTypeVar() + || type.isArray() + || type.is("java.lang.Object"); + } + + private static boolean areNeitherInterfaces(Type type1, Type type2) { + return !type1.symbol().isInterface() && !type2.symbol().isInterface(); + } + + private static boolean areTypesFinalClassAndInterface(Type type1, Type type2) { + return (type1.symbol().isInterface() && type2.symbol().isFinal()) + || (type2.symbol().isInterface() && type1.symbol().isFinal()); + } + +} diff --git a/java-checks/src/test/java/org/sonar/java/checks/InappropriateCastCheckTest.java b/java-checks/src/test/java/org/sonar/java/checks/InappropriateCastCheckTest.java new file mode 100644 index 00000000000..3dd3f5e2f3b --- /dev/null +++ b/java-checks/src/test/java/org/sonar/java/checks/InappropriateCastCheckTest.java @@ -0,0 +1,34 @@ +/* + * SonarQube Java + * Copyright (C) SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * You can redistribute and/or modify this program under the terms of + * the Sonar Source-Available License Version 1, as published by SonarSource Sàrl. + * + * 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 Sonar Source-Available License for more details. + * + * You should have received a copy of the Sonar Source-Available License + * along with this program; if not, see https://sonarsource.com/license/ssal/ + */ +package org.sonar.java.checks; + +import org.junit.jupiter.api.Test; +import org.sonar.java.checks.verifier.CheckVerifier; + +import static org.sonar.java.checks.verifier.TestUtils.mainCodeSourcesPath; + +class InappropriateCastCheckTest { + + @Test + void test() { + CheckVerifier.newVerifier() + .onFile(mainCodeSourcesPath("checks/InappropriateCastCheckSample.java")) + .withCheck(new InappropriateCastCheck()) + .verifyIssues(); + } + +} diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S1944.html b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S1944.html new file mode 100644 index 00000000000..a2c6b4c46f7 --- /dev/null +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S1944.html @@ -0,0 +1,45 @@ +

Why is this an issue?

+

Inappropriate casts are errors that will lead to bugs as the members are accessed. This includes casts from one unrelated type to another, as well +as untested casts down an inheritance hierarchy.

+

Noncompliant code example

+
+public class S1944 {
+
+  public static void main(String[] args) {
+    List<String> list = (List<String>) getAttributes(); // Noncompliant; List<Integer> return by getAttributes() is not be casted to List<String>
+    String s = list.get(0); // java.lang.ClassCastException will be raised here
+  }
+
+  private static List<?> getAttributes() {
+    List<Integer> result = new ArrayList<>();
+    result.add(0);
+    return result;
+  }
+
+}
+
+

Compliant solution

+
+public class S1944 {
+
+  public static void main(String[] args) {
+    List<Integer> list = (List<Integer>) getAttributes(); // Compliant
+    String s = String.valueOf(list.get(0));
+  }
+
+  private static List<?> getAttributes() {
+    List<Integer> result = new ArrayList<>();
+    result.add(0);
+    return result;
+  }
+
+}
+
+

Resources

+ + diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S1944.json b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S1944.json new file mode 100644 index 00000000000..4d80775c783 --- /dev/null +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S1944.json @@ -0,0 +1,34 @@ +{ + "title": "Inappropriate casts should not be made", + "type": "CODE_SMELL", + "code": { + "impacts": { + "MAINTAINABILITY": "MEDIUM" + }, + "attribute": "LOGICAL" + }, + "status": "ready", + "remediation": { + "func": "Constant\/Issue", + "constantCost": "20min" + }, + "tags": [ + "cwe", + "cert", + "suspicious" + ], + "defaultSeverity": "Major", + "ruleSpecification": "RSPEC-1944", + "sqKey": "S1944", + "scope": "All", + "securityStandards": { + "CERT": [ + "EXP36-C." + ], + "CWE": [ + 588, + 704 + ] + }, + "quickfix": "unknown" +} diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/Sonar_agentic_AI_profile.json b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/Sonar_agentic_AI_profile.json new file mode 100644 index 00000000000..90860463c6e --- /dev/null +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/Sonar_agentic_AI_profile.json @@ -0,0 +1,6 @@ +{ + "name": "Sonar agentic AI", + "ruleKeys": [ + "S1944" + ] +} diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/Sonar_way_profile.json b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/Sonar_way_profile.json new file mode 100644 index 00000000000..6e2e38cbb6d --- /dev/null +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/Sonar_way_profile.json @@ -0,0 +1,6 @@ +{ + "name": "Sonar way", + "ruleKeys": [ + "S1944" + ] +} diff --git a/sonar-java-plugin/src/main/resources/profiles/Sonar_agentic_AI/S1944 b/sonar-java-plugin/src/main/resources/profiles/Sonar_agentic_AI/S1944 new file mode 100644 index 00000000000..e69de29bb2d diff --git a/sonar-java-plugin/src/main/resources/profiles/Sonar_way/S1944 b/sonar-java-plugin/src/main/resources/profiles/Sonar_way/S1944 new file mode 100644 index 00000000000..e69de29bb2d