Skip to content
Merged
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
Expand Up @@ -30,16 +30,26 @@
* @since 0.1.0
* */
enum CoreRule {

AVOID_CHECKPANIC(RuleFactory.createRule(1, "Avoid checkpanic", RuleKind.CODE_SMELL)),
UNUSED_FUNCTION_PARAMETER(RuleFactory.createRule(2, "Unused function parameter", RuleKind.CODE_SMELL)),
UNUSED_FUNCTION_PARAMETER(RuleFactory.createRule(2,
"Unused function parameter", RuleKind.CODE_SMELL)),
PUBLIC_NON_ISOLATED_FUNCTION_CONSTRUCT(RuleFactory.createRule(3,
"Non isolated public function", RuleKind.CODE_SMELL)),
PUBLIC_NON_ISOLATED_METHOD_CONSTRUCT(RuleFactory.createRule(4,
"Non isolated public method", RuleKind.CODE_SMELL)),
PUBLIC_NON_ISOLATED_CLASS_CONSTRUCT(RuleFactory.createRule(5,
"Non isolated public class", RuleKind.CODE_SMELL)),
PUBLIC_NON_ISOLATED_OBJECT_CONSTRUCT(RuleFactory.createRule(6,
"Non isolated public object", RuleKind.CODE_SMELL));
"Non isolated public object", RuleKind.CODE_SMELL)),
OPERATION_ALWAYS_EVALUATES_TO_TRUE(RuleFactory.createRule(7,
"This operation always evaluates to true", RuleKind.CODE_SMELL)),
OPERATION_ALWAYS_EVALUATES_TO_FALSE(RuleFactory.createRule(8,
"This operation always evaluates to false", RuleKind.CODE_SMELL)),
OPERATION_ALWAYS_EVALUATES_TO_SELF_VALUE(RuleFactory.createRule(9,
"This operation always evaluates to the same value", RuleKind.CODE_SMELL)),
SELF_ASSIGNMENT(RuleFactory.createRule(10,
"This variable is assigned to itself", RuleKind.CODE_SMELL));

private final Rule rule;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,11 @@
import io.ballerina.compiler.api.symbols.SymbolKind;
import io.ballerina.compiler.api.symbols.TypeDefinitionSymbol;
import io.ballerina.compiler.api.symbols.TypeSymbol;
import io.ballerina.compiler.syntax.tree.AssignmentStatementNode;
import io.ballerina.compiler.syntax.tree.BinaryExpressionNode;
import io.ballerina.compiler.syntax.tree.CheckExpressionNode;
import io.ballerina.compiler.syntax.tree.ClassDefinitionNode;
import io.ballerina.compiler.syntax.tree.CompoundAssignmentStatementNode;
import io.ballerina.compiler.syntax.tree.ExplicitAnonymousFunctionExpressionNode;
import io.ballerina.compiler.syntax.tree.FunctionDefinitionNode;
import io.ballerina.compiler.syntax.tree.FunctionSignatureNode;
Expand All @@ -44,6 +47,7 @@
import io.ballerina.compiler.syntax.tree.TypeDefinitionNode;
import io.ballerina.projects.Document;
import io.ballerina.scan.ScannerContext;
import io.ballerina.scan.utils.Constants;

import java.util.List;
import java.util.Optional;
Expand All @@ -52,6 +56,9 @@
import static io.ballerina.compiler.syntax.tree.SyntaxKind.PUBLIC_KEYWORD;
import static io.ballerina.scan.utils.Constants.INIT_FUNCTION;
import static io.ballerina.scan.utils.Constants.MAIN_FUNCTION;
import static io.ballerina.scan.utils.ScanCodeAnalyzerUtils.isDefinedQualifiedNameReference;
import static io.ballerina.scan.utils.ScanCodeAnalyzerUtils.isEqualToProvidedLiteralIdentifier;
import static io.ballerina.scan.utils.ScanCodeAnalyzerUtils.isSameSimpleExpression;

/**
* {@code StaticCodeAnalyzer} contains the logic to perform core static code analysis on Ballerina documents.
Expand Down Expand Up @@ -88,6 +95,159 @@ public void visit(CheckExpressionNode checkExpressionNode) {
}

@Override
public void visit(BinaryExpressionNode binaryExpressionNode) {
reportIssuesWithTrivialOperations(binaryExpressionNode);
filterSameReferenceIssueBasedOnOperandType(binaryExpressionNode.operator()).ifPresent(rule -> {
checkUsageOfSameOperandInBinaryExpr(binaryExpressionNode.lhsExpr(),
binaryExpressionNode.rhsExpr(), rule, binaryExpressionNode);
});
}

@Override
public void visit(AssignmentStatementNode assignmentStatementNode) {
checkUsageOfAssignment(assignmentStatementNode.varRef(), assignmentStatementNode.expression(),
CoreRule.SELF_ASSIGNMENT, assignmentStatementNode);
this.visitSyntaxNode(assignmentStatementNode);
}

@Override
public void visit(CompoundAssignmentStatementNode compoundAssignmentStatementNode) {
checkUsageOfAssignment(compoundAssignmentStatementNode.lhsExpression(),
compoundAssignmentStatementNode.rhsExpression(), CoreRule.SELF_ASSIGNMENT,
compoundAssignmentStatementNode);
this.visitSyntaxNode(compoundAssignmentStatementNode);
}

private void checkUsageOfSameOperandInBinaryExpr(Node lhs, Node rhs, CoreRule rule, Node binaryExprNode) {
if (isSameSimpleExpression(lhs, rhs)) {
reportIssue(binaryExprNode, rule);
}
}

private void checkUsageOfAssignment(Node lhs, Node rhs, CoreRule rule, Node assignmentNode) {
if (isSameSimpleExpression(lhs, rhs)) {
reportIssue(assignmentNode, rule);
}
}

private void reportIssuesWithTrivialOperations(BinaryExpressionNode binaryExpressionNode) {
SyntaxKind binaryOperatorKind = binaryExpressionNode.operator().kind();
if (binaryOperatorKind == SyntaxKind.GT_TOKEN) {
if (isDefinedQualifiedNameReference(binaryExpressionNode.rhsExpr(),
Constants.Token.FLOAT, Constants.Token.INFINITY)) {
// a > Infinity is always false.
reportIssue(binaryExpressionNode, CoreRule.OPERATION_ALWAYS_EVALUATES_TO_FALSE);
}

if (isDefinedQualifiedNameReference(binaryExpressionNode.rhsExpr(),
Constants.Token.INT, Constants.Token.MAX_VALUE)) {
// a > MAX_VALUE is always false.
reportIssue(binaryExpressionNode, CoreRule.OPERATION_ALWAYS_EVALUATES_TO_FALSE);
}
}

if (binaryOperatorKind == SyntaxKind.LT_EQUAL_TOKEN) {
if (isDefinedQualifiedNameReference(binaryExpressionNode.rhsExpr(),
Constants.Token.FLOAT, Constants.Token.INFINITY)) {
// a <= Infinity is always true.
reportIssue(binaryExpressionNode, CoreRule.OPERATION_ALWAYS_EVALUATES_TO_TRUE);
}

if (isDefinedQualifiedNameReference(binaryExpressionNode.rhsExpr(),
Constants.Token.INT, Constants.Token.MAX_VALUE)) {
// a <= MAX_VALUE is always true.
reportIssue(binaryExpressionNode, CoreRule.OPERATION_ALWAYS_EVALUATES_TO_TRUE);
}
}

if (binaryOperatorKind == SyntaxKind.LT_TOKEN) {
if (isDefinedQualifiedNameReference(binaryExpressionNode.rhsExpr(),
Constants.Token.INT, Constants.Token.MIN_VALUE)) {
// a < MIN_VALUE is always false.
reportIssue(binaryExpressionNode, CoreRule.OPERATION_ALWAYS_EVALUATES_TO_FALSE);
}
}

if (binaryOperatorKind == SyntaxKind.GT_EQUAL_TOKEN) {
if (isDefinedQualifiedNameReference(binaryExpressionNode.rhsExpr(),
Constants.Token.INT, Constants.Token.MIN_VALUE)) {
// a >= MIN_VALUE is always true.
reportIssue(binaryExpressionNode, CoreRule.OPERATION_ALWAYS_EVALUATES_TO_TRUE);
}
}

if (binaryOperatorKind == SyntaxKind.LOGICAL_AND_TOKEN) {
if (isEqualToProvidedLiteralIdentifier(binaryExpressionNode.rhsExpr(), Constants.Token.FALSE)
|| isEqualToProvidedLiteralIdentifier(binaryExpressionNode.lhsExpr(), Constants.Token.FALSE)) {
// a && false is always false.
reportIssue(binaryExpressionNode, CoreRule.OPERATION_ALWAYS_EVALUATES_TO_FALSE);
}

if (isEqualToProvidedLiteralIdentifier(binaryExpressionNode.rhsExpr(), Constants.Token.TRUE)
|| isEqualToProvidedLiteralIdentifier(binaryExpressionNode.lhsExpr(), Constants.Token.TRUE)) {
// a && true is always `a`.
reportIssue(binaryExpressionNode, CoreRule.OPERATION_ALWAYS_EVALUATES_TO_SELF_VALUE);
}
}

if (binaryOperatorKind == SyntaxKind.LOGICAL_OR_TOKEN) {
if (isEqualToProvidedLiteralIdentifier(binaryExpressionNode.rhsExpr(), Constants.Token.FALSE)
|| isEqualToProvidedLiteralIdentifier(binaryExpressionNode.lhsExpr(), Constants.Token.FALSE)) {
// a || false is always `a`.
reportIssue(binaryExpressionNode, CoreRule.OPERATION_ALWAYS_EVALUATES_TO_SELF_VALUE);
}

if (isEqualToProvidedLiteralIdentifier(binaryExpressionNode.rhsExpr(), Constants.Token.TRUE)
|| isEqualToProvidedLiteralIdentifier(binaryExpressionNode.lhsExpr(), Constants.Token.TRUE)) {
// a || true is always true.
reportIssue(binaryExpressionNode, CoreRule.OPERATION_ALWAYS_EVALUATES_TO_TRUE);
}
}

if (binaryOperatorKind == SyntaxKind.BITWISE_AND_TOKEN) {
if (isEqualToProvidedLiteralIdentifier(binaryExpressionNode.rhsExpr(), Constants.Token.ZERO)
|| isEqualToProvidedLiteralIdentifier(binaryExpressionNode.lhsExpr(), Constants.Token.ZERO)) {
// a & 0 is always false.
reportIssue(binaryExpressionNode, CoreRule.OPERATION_ALWAYS_EVALUATES_TO_FALSE);
}

if (isEqualToProvidedLiteralIdentifier(binaryExpressionNode.rhsExpr(), Constants.Token.MINUS_ONE)
|| isEqualToProvidedLiteralIdentifier(binaryExpressionNode.lhsExpr(), Constants.Token.MINUS_ONE)) {
// a & -1 is always `a`.
reportIssue(binaryExpressionNode, CoreRule.OPERATION_ALWAYS_EVALUATES_TO_SELF_VALUE);
}
}

if (binaryOperatorKind == SyntaxKind.PIPE_TOKEN) {
if (isEqualToProvidedLiteralIdentifier(binaryExpressionNode.rhsExpr(), Constants.Token.ZERO)
|| isEqualToProvidedLiteralIdentifier(binaryExpressionNode.lhsExpr(), Constants.Token.ZERO)) {
// a | 0 is always `a`.
reportIssue(binaryExpressionNode, CoreRule.OPERATION_ALWAYS_EVALUATES_TO_SELF_VALUE);
}

if (isEqualToProvidedLiteralIdentifier(binaryExpressionNode.rhsExpr(), Constants.Token.MINUS_ONE)
|| isEqualToProvidedLiteralIdentifier(binaryExpressionNode.lhsExpr(), Constants.Token.MINUS_ONE)) {
// a | -1 is always true.
reportIssue(binaryExpressionNode, CoreRule.OPERATION_ALWAYS_EVALUATES_TO_TRUE);
}
}
}

private Optional<CoreRule> filterSameReferenceIssueBasedOnOperandType(Token operator) {
switch (operator.kind()) {
case GT_EQUAL_TOKEN, LT_EQUAL_TOKEN, DOUBLE_EQUAL_TOKEN, TRIPPLE_EQUAL_TOKEN -> {
return Optional.of(CoreRule.OPERATION_ALWAYS_EVALUATES_TO_TRUE);
}
case GT_TOKEN, LT_TOKEN, NOT_DOUBLE_EQUAL_TOKEN, NOT_EQUAL_TOKEN -> {
return Optional.of(CoreRule.OPERATION_ALWAYS_EVALUATES_TO_FALSE);
}
case LOGICAL_OR_TOKEN, LOGICAL_AND_TOKEN, BITWISE_AND_TOKEN, PIPE_TOKEN -> {
return Optional.of(CoreRule.OPERATION_ALWAYS_EVALUATES_TO_SELF_VALUE);
}
}
return Optional.empty();
}

public void visit(FunctionDefinitionNode functionDefinitionNode) {
checkUnusedFunctionParameters(functionDefinitionNode.functionSignature());
String functionName = functionDefinitionNode.functionName().text();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,21 @@ public class Constants {
public static final String MAIN_FUNCTION = "main";
public static final String INIT_FUNCTION = "init";

public static class Token {
public static final String FLOAT = "float";
public static final String INT = "int";
public static final String INFINITY = "Infinity";
public static final String MAX_VALUE = "MAX_VALUE";
public static final String MIN_VALUE = "MIN_VALUE";
public static final String TRUE = "true";
public static final String FALSE = "false";
public static final String ZERO = "0";
public static final String ONE = "1";
public static final String MINUS_ONE = "-1";
private Token() {
}
}

private Constants() {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/*
* Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com).
*
* WSO2 LLC. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package io.ballerina.scan.utils;
Comment thread
SasinduDilshara marked this conversation as resolved.

import io.ballerina.compiler.syntax.tree.BasicLiteralNode;
import io.ballerina.compiler.syntax.tree.BuiltinSimpleNameReferenceNode;
import io.ballerina.compiler.syntax.tree.ExpressionNode;
import io.ballerina.compiler.syntax.tree.FieldAccessExpressionNode;
import io.ballerina.compiler.syntax.tree.IndexedExpressionNode;
import io.ballerina.compiler.syntax.tree.Node;
import io.ballerina.compiler.syntax.tree.QualifiedNameReferenceNode;
import io.ballerina.compiler.syntax.tree.SeparatedNodeList;
import io.ballerina.compiler.syntax.tree.SimpleNameReferenceNode;
import io.ballerina.compiler.syntax.tree.SyntaxKind;
import io.ballerina.compiler.syntax.tree.UnaryExpressionNode;
import io.ballerina.projects.Document;
import io.ballerina.scan.Rule;
import io.ballerina.scan.ScannerContext;
import io.ballerina.tools.diagnostics.Location;

/**
* {@code ScanCodeAnalyzerUtils} contains the util functions for the static code analysis.
*
* @since 0.1.0
* */
public class ScanCodeAnalyzerUtils {

Check warning on line 41 in scan-command/src/main/java/io/ballerina/scan/utils/ScanCodeAnalyzerUtils.java

View check run for this annotation

Codecov / codecov/patch

scan-command/src/main/java/io/ballerina/scan/utils/ScanCodeAnalyzerUtils.java#L41

Added line #L41 was not covered by tests
Comment thread
SasinduDilshara marked this conversation as resolved.
public static void reportIssue(ScannerContext scannerContext, Document document, Node node, Rule rule) {
scannerContext.getReporter().reportIssue(document, node.location(), rule);
}

Check warning on line 44 in scan-command/src/main/java/io/ballerina/scan/utils/ScanCodeAnalyzerUtils.java

View check run for this annotation

Codecov / codecov/patch

scan-command/src/main/java/io/ballerina/scan/utils/ScanCodeAnalyzerUtils.java#L43-L44

Added lines #L43 - L44 were not covered by tests

public static void reportIssue(ScannerContext scannerContext, Document document, Location location, Rule rule) {
scannerContext.getReporter().reportIssue(document, location, rule);
}

Check warning on line 48 in scan-command/src/main/java/io/ballerina/scan/utils/ScanCodeAnalyzerUtils.java

View check run for this annotation

Codecov / codecov/patch

scan-command/src/main/java/io/ballerina/scan/utils/ScanCodeAnalyzerUtils.java#L47-L48

Added lines #L47 - L48 were not covered by tests

public static boolean isSameSimpleExpression(Node n1, Node n2) {
if (n1 instanceof SimpleNameReferenceNode lhsExp && n2 instanceof SimpleNameReferenceNode rhsExpr) {
return lhsExp.name().text().equals(rhsExpr.name().text());
}

if (n1 instanceof QualifiedNameReferenceNode lhsExp && n2 instanceof QualifiedNameReferenceNode rhsExpr) {
return lhsExp.modulePrefix() != null && rhsExpr.modulePrefix() != null
&& lhsExp.modulePrefix().text().equals(rhsExpr.modulePrefix().text())
&& lhsExp.identifier().text().equals(rhsExpr.identifier().text());
}

if (n1 instanceof FieldAccessExpressionNode lhsExp && n2 instanceof FieldAccessExpressionNode rhsExpr) {
// only the simple field access expressions will be considered.
return isSameSimpleExpression(lhsExp.fieldName(), rhsExpr.fieldName())
&& isSameSimpleExpression(lhsExp.expression(), rhsExpr.expression());
}

if (n1 instanceof IndexedExpressionNode lhsExp && n2 instanceof IndexedExpressionNode rhsExpr) {
// only the simple field access expressions will be considered.
return isSameSimpleExpression(lhsExp.containerExpression(), rhsExpr.containerExpression())
&& isSameKeyExpression(lhsExp.keyExpression(), rhsExpr.keyExpression());
}

if (n1 instanceof BuiltinSimpleNameReferenceNode lhsExp
&& n2 instanceof BuiltinSimpleNameReferenceNode rhsExpr) {
return lhsExp.name().text().equals(rhsExpr.name().text());

Check warning on line 75 in scan-command/src/main/java/io/ballerina/scan/utils/ScanCodeAnalyzerUtils.java

View check run for this annotation

Codecov / codecov/patch

scan-command/src/main/java/io/ballerina/scan/utils/ScanCodeAnalyzerUtils.java#L74-L75

Added lines #L74 - L75 were not covered by tests
}

if (n1 instanceof BasicLiteralNode lhsExp && n2 instanceof BasicLiteralNode rhsExpr) {
return lhsExp.literalToken().text().equals(rhsExpr.literalToken().text());
}

return false;
}

private static boolean isSameKeyExpression(SeparatedNodeList<ExpressionNode> lhsKeyExpNodes,
SeparatedNodeList<ExpressionNode> rhsKeyExpNodes) {
if (lhsKeyExpNodes.size() != rhsKeyExpNodes.size()) {
return false;

Check warning on line 88 in scan-command/src/main/java/io/ballerina/scan/utils/ScanCodeAnalyzerUtils.java

View check run for this annotation

Codecov / codecov/patch

scan-command/src/main/java/io/ballerina/scan/utils/ScanCodeAnalyzerUtils.java#L88

Added line #L88 was not covered by tests
}

for (int i = 0; i < lhsKeyExpNodes.size(); i++) {
if (!isSameSimpleExpression(lhsKeyExpNodes.get(i), rhsKeyExpNodes.get(i))) {
return false;
}
}
return true;
}

public static boolean isDefinedQualifiedNameReference(Node node, String module, String identifier) {
if (node instanceof QualifiedNameReferenceNode ref) {
return ref.modulePrefix() != null && ref.identifier() != null
&& ref.modulePrefix().text().equals(module) && ref.identifier().text().equals(identifier);
}
return false;
}

public static boolean isEqualToProvidedLiteralIdentifier(Node node, String identifierName) {
if (identifierName.equals(Constants.Token.MINUS_ONE)) {
if (node instanceof UnaryExpressionNode exp) {
return exp.unaryOperator().kind().equals(SyntaxKind.MINUS_TOKEN)
&& isEqualToProvidedLiteralIdentifier(exp.expression(), Constants.Token.ONE);
}
return false;
}
if (node instanceof BasicLiteralNode ref) {
return ref.literalToken().text().equals(identifierName);
}
return false;
}
}
Loading
Loading