Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
package checks;

import java.io.Serializable;
import java.util.function.Supplier;

class ArrayCovarianceCheckSample {

abstract static class Fruit {}
static class Apple extends Fruit {}
static class Orange extends Fruit {}

abstract static class Shape {}
static class Circle extends Shape {}

static class A {}
static class B extends A {}
static class C extends B {}

// --- Noncompliant: variable declarations with new array creation ---

void variableDeclarations() {
Fruit[] fruits1 = new Apple[10]; // Noncompliant {{Use the type of the actual array element here; array covariance can lead to ArrayStoreException at runtime.}}
// ^^^^^^^^^^^^^
Object[] objects = new String[5]; // Noncompliant
Number[] numbers = new Integer[4]; // Noncompliant
CharSequence[] seqs = new String[3]; // Noncompliant
Serializable[] items = new String[2]; // Noncompliant
A[] deepHierarchy = new C[3]; // Noncompliant
}

// --- Noncompliant: variable initialized from existing subtype array reference ---

void variableFromReference() {
Apple[] apples = new Apple[5];
Fruit[] fruits = apples; // Noncompliant
}

// --- Noncompliant: field-level covariance ---

Shape[] shapeField = new Circle[2]; // Noncompliant

// --- Noncompliant: assignments ---

void assignments() {
Fruit[] fruits;
fruits = new Apple[5]; // Noncompliant
Object[] objects;
objects = new Integer[3]; // Noncompliant
}

// --- Noncompliant: return statements ---

Fruit[] returnCovariant() {
return new Apple[1]; // Noncompliant
}

Object[] returnCovariantJdk() {
return new String[2]; // Noncompliant
}

// --- Noncompliant: method arguments ---

void acceptFruits(Fruit[] fruits) {}
void acceptObjects(Object[] objects) {}

void methodArguments() {
acceptFruits(new Apple[1]); // Noncompliant
acceptObjects(new String[1]); // Noncompliant
}

// --- Noncompliant: constructor arguments ---

static class Container {
Container(Fruit[] fruits) {}
Container(int x, Object[] objects) {}
}

void constructorArguments() {
new Container(new Apple[1]); // Noncompliant
new Container(1, new String[2]); // Noncompliant
}

// --- Noncompliant: lambda return ---

void lambdaReturn() {
Supplier<Fruit[]> s = () -> {
return new Apple[1]; // Noncompliant
};
}

// --- Noncompliant: switch expression ---

void switchExpression(int code) {
Apple[] apples = new Apple[1];
Fruit[] result = switch (code) {
case 0 -> apples; // Noncompliant
default -> null;
};
}

// --- Noncompliant: yield statement ---

void yieldStatement(int code) {
Apple[] apples = new Apple[1];
Fruit[] result = switch (code) {
case 0 -> null;
default -> {
yield apples; // Noncompliant
}
};
}

// --- Compliant: same-type arrays ---

void sameType() {
Apple[] apples = new Apple[10]; // Compliant
String[] strings = new String[5]; // Compliant
Fruit[] fruits = new Fruit[3]; // Compliant
}

// --- Compliant: primitive arrays ---

void primitiveArrays() {
int[] numbers = new int[4]; // Compliant
double[] doubles = new double[3]; // Compliant
}

// --- Compliant: null assignment ---

void nullAssignment() {
Object[] objects = null; // Compliant
}

// --- Compliant: method return matching declared type ---

Apple[] returnSameType() {
return new Apple[1]; // Compliant
}

// --- Compliant: method argument matching parameter type ---

void acceptApples(Apple[] apples) {}

void methodArgumentsSameType() {
acceptApples(new Apple[1]); // Compliant
}

// --- Compliant: multi-dimensional same-type ---

void multiDimensional() {
String[][] matrix = new String[3][3]; // Compliant
}

// --- Compliant: base type array creation with initializer ---

void baseTypeInitializer() {
Number[] numbers = new Number[] { 1, 2.0 }; // Compliant
}

// --- Compliant: void return ---

void voidReturn() {
return; // Compliant
}

// --- Compliant: non-array types ---

void nonArrayTypes() {
Object o = "hello"; // Compliant
Number n = 42; // Compliant
}

// --- Compliant: lambda returning matching type ---

void lambdaCompliant() {
Supplier<Apple[]> s = () -> {
return new Apple[1]; // Compliant
};
}

// --- Compliant: switch expression with matching types ---

void switchCompliant(int code, Fruit[] fruits) {
Fruit[] result = switch (code) {
case 0 -> fruits; // Compliant
default -> null;
};
}

// --- Compliant: switch statement yield (not expression) ---

void switchStatement(int code) {
switch (code) {
default -> doNothing();
};
}

private void doNothing() {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/*
* 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.List;
import org.sonar.check.Rule;
import org.sonar.java.model.ExpressionUtils;
import org.sonar.plugins.java.api.IssuableSubscriptionVisitor;
import org.sonar.plugins.java.api.semantic.Symbol;
import org.sonar.plugins.java.api.semantic.Type;
import org.sonar.plugins.java.api.tree.Arguments;
import org.sonar.plugins.java.api.tree.AssignmentExpressionTree;
import org.sonar.plugins.java.api.tree.ExpressionTree;
import org.sonar.plugins.java.api.tree.LambdaExpressionTree;
import org.sonar.plugins.java.api.tree.MethodInvocationTree;
import org.sonar.plugins.java.api.tree.MethodTree;
import org.sonar.plugins.java.api.tree.NewClassTree;
import org.sonar.plugins.java.api.tree.ReturnStatementTree;
import org.sonar.plugins.java.api.tree.Tree;
import org.sonar.plugins.java.api.tree.VariableTree;
import org.sonar.plugins.java.api.tree.YieldStatementTree;

@Rule(key = "S2330")
public class ArrayCovarianceCheck extends IssuableSubscriptionVisitor {

private static final String MESSAGE = "Use the type of the actual array element here; array covariance can lead to ArrayStoreException at runtime.";

@Override
public List<Tree.Kind> nodesToVisit() {
return List.of(Tree.Kind.VARIABLE, Tree.Kind.ASSIGNMENT, Tree.Kind.RETURN_STATEMENT,
Tree.Kind.YIELD_STATEMENT, Tree.Kind.METHOD_INVOCATION, Tree.Kind.NEW_CLASS);
}

@Override
public void visitNode(Tree tree) {
switch (tree.kind()) {
case VARIABLE -> visitVariable((VariableTree) tree);
case ASSIGNMENT -> {
var assignment = (AssignmentExpressionTree) tree;
checkArrayCovariance(assignment.variable().symbolType(), assignment.expression());
}
case RETURN_STATEMENT -> visitReturnStatement((ReturnStatementTree) tree);
case YIELD_STATEMENT -> visitYieldStatement((YieldStatementTree) tree);
case METHOD_INVOCATION -> {
var invocation = (MethodInvocationTree) tree;
visitInvocation(invocation.methodSymbol(), invocation.arguments());
}
case NEW_CLASS -> {
var invocation = (NewClassTree) tree;
visitInvocation(invocation.methodSymbol(), invocation.arguments());
}
default -> {
// do nothing
}
}
}

private void visitVariable(VariableTree tree) {
var initializer = tree.initializer();
if (initializer != null) {
checkArrayCovariance(tree.symbol().type(), initializer);
}
}

private void visitReturnStatement(ReturnStatementTree tree) {
var expression = tree.expression();
if (expression == null) {
return;
}
Tree enclosing = ExpressionUtils.getEnclosingTree(tree, Tree.Kind.METHOD, Tree.Kind.LAMBDA_EXPRESSION);
if (enclosing != null) {
var lhsType = enclosing instanceof LambdaExpressionTree lambda
? lambda.symbol().returnType().type()
: ((MethodTree) enclosing).returnType().symbolType();
checkArrayCovariance(lhsType, expression);
}
}
Comment on lines +79 to +91

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Edge Case: Expression-bodied lambdas escape covariance detection

The return-path handling only fires on Tree.Kind.RETURN_STATEMENT, but an expression-bodied lambda such as Supplier<Fruit[]> s = () -> new Apple[1]; has no return statement node (its body is an ExpressionTree, not a block with a return). Such covariant lambda bodies are therefore never checked, a false negative compared to the block-lambda case that is tested at ArrayCovarianceCheckSample.java:85-89. Consider also handling LAMBDA_EXPRESSION nodes directly: when the body is an ExpressionTree, compare lambda.symbol().returnType().type() against the body expression's type.

Was this helpful? React with 👍 / 👎


private void visitYieldStatement(YieldStatementTree tree) {
Tree enclosing = ExpressionUtils.getEnclosingTree(tree, Tree.Kind.SWITCH_EXPRESSION, Tree.Kind.SWITCH_STATEMENT);
if (enclosing == null || enclosing.is(Tree.Kind.SWITCH_STATEMENT)) {
return;
}
var lhsType = ((ExpressionTree) enclosing).symbolType();
checkArrayCovariance(lhsType, tree.expression());
}

private void visitInvocation(Symbol.MethodSymbol methodSymbol, Arguments arguments) {
List<Type> parameterTypes = methodSymbol.parameterTypes();
var nonVarargCount = parameterTypes.size() - (methodSymbol.isVarArgsMethod() ? 1 : 0);
for (int i = 0; i < nonVarargCount && i < arguments.size(); i++) {
checkArrayCovariance(parameterTypes.get(i), arguments.get(i));
}
if (!methodSymbol.isVarArgsMethod() || arguments.size() == nonVarargCount) {
return;
}
var varargType = (Type.ArrayType) parameterTypes.get(nonVarargCount);
checkArrayCovariance(varargType, arguments.get(nonVarargCount));
var elementType = varargType.elementType();
for (int i = nonVarargCount; i < arguments.size(); i++) {
checkArrayCovariance(elementType, arguments.get(i));
}
}

private void checkArrayCovariance(Type lhsType, ExpressionTree rhsExpression) {
var rhsType = rhsExpression.symbolType();
if (!lhsType.isArray() || !rhsType.isArray() || rhsType.isNullType() || rhsType.isUnknown() || lhsType.isUnknown()) {
return;
}
var lhsElementType = ((Type.ArrayType) lhsType).elementType();
var rhsElementType = ((Type.ArrayType) rhsType).elementType();
if (lhsElementType.isUnknown() || rhsElementType.isUnknown() || lhsElementType.isPrimitive() || rhsElementType.isPrimitive()) {
return;
}
if (rhsElementType.isSubtypeOf(lhsElementType) && !lhsElementType.isSubtypeOf(rhsElementType)) {
context.reportIssue(this, rhsExpression, MESSAGE);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* 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 ArrayCovarianceCheckTest {

@Test
void test() {
CheckVerifier.newVerifier()
.onFile(mainCodeSourcesPath("checks/ArrayCovarianceCheckSample.java"))
.withCheck(new ArrayCovarianceCheck())
.verifyIssues();
}
}
Loading
Loading