diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d03f4d71..62e51a98 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -20,7 +20,8 @@ sonar-plugin-api = { group = "org.sonarsource.api.plugin", name = "sonar-plugin- sonar-analyzer-commons = { group = "org.sonarsource.analyzer-commons", name = "sonar-analyzer-commons", version.ref = "analyzer-commons" } sonar-analyzer-test-commons = { group = "org.sonarsource.analyzer-commons", name = "sonar-analyzer-test-commons", version.ref = "analyzer-commons" } slang-api = { group = "org.sonarsource.slang", name = "slang-api", version.ref = "slang-dependencies" } -slang-checks = { group = "org.sonarsource.slang", name = "slang-checks", version.ref = "slang-dependencies" } +slang-antlr = { group = "org.sonarsource.slang", name = "slang-antlr", version.ref = "slang-dependencies" } +slang-testing = { group = "org.sonarsource.slang", name = "slang-testing", version.ref = "slang-dependencies" } checkstyle-import = { group = "org.sonarsource.slang", name = "checkstyle-import", version.ref = "slang-dependencies" } minimal-json = { group = "com.eclipsesource.minimal-json", name = "minimal-json", version.ref = "minimal-json" } sonar-plugin-api-test-fixtures = { group = "org.sonarsource.api.plugin", name = "sonar-plugin-api-test-fixtures", version.ref = "plugin-api" } diff --git a/its/ruling/src/integrationTest/resources/expected/go-ParsingError.json b/its/ruling/src/integrationTest/resources/expected/go-S2260.json similarity index 100% rename from its/ruling/src/integrationTest/resources/expected/go-ParsingError.json rename to its/ruling/src/integrationTest/resources/expected/go-S2260.json diff --git a/settings.gradle.kts b/settings.gradle.kts index cee7a057..decfd367 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -26,5 +26,7 @@ rootProject.name = "sonar-go" include(":sonar-go-to-slang") include(":sonar-go-plugin") +include(":sonar-go-checks") +include(":sonar-go-commons") include(":its:plugin") include(":its:ruling") diff --git a/sonar-go-checks/build.gradle.kts b/sonar-go-checks/build.gradle.kts new file mode 100644 index 00000000..ba50851f --- /dev/null +++ b/sonar-go-checks/build.gradle.kts @@ -0,0 +1,41 @@ +/* + * SonarSource Go + * Copyright (C) 2018-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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/ + */ +plugins { + id("org.sonarsource.cloud-native.java-conventions") + id("org.sonarsource.cloud-native.code-style-conventions") +} + +dependencies { + compileOnly(libs.sonar.plugin.api) + + implementation(project(":sonar-go-to-slang", configuration = "goBinaries")) + implementation(libs.sonar.analyzer.commons) + implementation(libs.slang.api) + + testImplementation(libs.assertj.core) + testImplementation(libs.mockito.core) + testImplementation(libs.slang.antlr) + testImplementation(libs.slang.testing) + testImplementation(libs.sonar.analyzer.test.commons) + testImplementation(libs.classgraph) + testImplementation(libs.junit.jupiter.api) + testImplementation(libs.sonar.plugin.api.impl) + testImplementation(libs.sonar.plugin.api.test.fixtures) + testImplementation(testFixtures(project(":sonar-go-commons"))) + + testRuntimeOnly(libs.junit.jupiter.engine) +} diff --git a/sonar-go-checks/src/main/java/org/sonar/go/checks/AbstractBranchDuplicationCheck.java b/sonar-go-checks/src/main/java/org/sonar/go/checks/AbstractBranchDuplicationCheck.java new file mode 100644 index 00000000..ff65d5c4 --- /dev/null +++ b/sonar-go-checks/src/main/java/org/sonar/go/checks/AbstractBranchDuplicationCheck.java @@ -0,0 +1,94 @@ +/* + * SonarSource Go + * Copyright (C) 2018-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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.go.checks; + +import java.util.ArrayList; +import java.util.List; +import org.sonarsource.slang.api.IfTree; +import org.sonarsource.slang.api.MatchCaseTree; +import org.sonarsource.slang.api.MatchTree; +import org.sonarsource.slang.api.Tree; +import org.sonarsource.slang.checks.api.CheckContext; +import org.sonarsource.slang.checks.api.InitContext; +import org.sonarsource.slang.checks.api.SlangCheck; + +import static org.sonarsource.slang.utils.SyntacticEquivalence.areEquivalent; + +public abstract class AbstractBranchDuplicationCheck implements SlangCheck { + + protected abstract void checkDuplicatedBranches(CheckContext ctx, Tree tree, List branches); + + protected abstract void onAllIdenticalBranches(CheckContext ctx, Tree tree); + + @Override + public void initialize(InitContext init) { + init.register(IfTree.class, (ctx, tree) -> { + Tree parent = ctx.parent(); + if (!(parent instanceof IfTree ifTree) || tree == ifTree.thenBranch()) { + checkConditionalStructure(ctx, tree, new ConditionalStructure(tree)); + } + }); + init.register(MatchTree.class, (ctx, tree) -> checkConditionalStructure(ctx, tree, new ConditionalStructure(tree))); + } + + protected void checkConditionalStructure(CheckContext ctx, Tree tree, ConditionalStructure conditional) { + if (conditional.allBranchesArePresent && conditional.allBranchesAreIdentical()) { + onAllIdenticalBranches(ctx, tree); + } else { + checkDuplicatedBranches(ctx, tree, conditional.branches); + } + } + + public static class ConditionalStructure { + + private boolean allBranchesArePresent = false; + + private final List branches = new ArrayList<>(); + + private ConditionalStructure(IfTree ifTree) { + branches.add(ifTree.thenBranch()); + Tree elseBranch = ifTree.elseBranch(); + while (elseBranch != null) { + if (elseBranch instanceof IfTree elseIf) { + branches.add(elseIf.thenBranch()); + elseBranch = elseIf.elseBranch(); + } else { + branches.add(elseBranch); + allBranchesArePresent = true; + elseBranch = null; + } + } + } + + private ConditionalStructure(MatchTree tree) { + for (MatchCaseTree caseTree : tree.cases()) { + branches.add(caseTree.body()); + if (caseTree.expression() == null) { + allBranchesArePresent = true; + } + } + } + + private boolean allBranchesAreIdentical() { + return branches.size() > 1 && + branches.stream() + .skip(1) + .allMatch(branch -> areEquivalent(branches.get(0), branch)); + } + } + +} diff --git a/sonar-go-plugin/src/main/java/org/sonar/go/checks/CodeAfterJumpGoCheck.java b/sonar-go-checks/src/main/java/org/sonar/go/checks/AllBranchesIdenticalCheck.java similarity index 57% rename from sonar-go-plugin/src/main/java/org/sonar/go/checks/CodeAfterJumpGoCheck.java rename to sonar-go-checks/src/main/java/org/sonar/go/checks/AllBranchesIdenticalCheck.java index e003f6f9..c01066cd 100644 --- a/sonar-go-plugin/src/main/java/org/sonar/go/checks/CodeAfterJumpGoCheck.java +++ b/sonar-go-checks/src/main/java/org/sonar/go/checks/AllBranchesIdenticalCheck.java @@ -16,25 +16,22 @@ */ package org.sonar.go.checks; +import java.util.List; import org.sonar.check.Rule; -import org.sonarsource.slang.api.NativeTree; import org.sonarsource.slang.api.Tree; -import org.sonarsource.slang.checks.CodeAfterJumpCheck; +import org.sonarsource.slang.checks.api.CheckContext; -import static org.sonar.go.checks.NativeKinds.LABEL; -import static org.sonar.go.checks.NativeKinds.SEMICOLON; +@Rule(key = "S3923") +public class AllBranchesIdenticalCheck extends AbstractBranchDuplicationCheck { -@Rule(key = "S1763") -public class CodeAfterJumpGoCheck extends CodeAfterJumpCheck { @Override - protected boolean isValidAfterJump(Tree tree) { - return tree instanceof NativeTree && - ((NativeTree) tree).nativeKind().toString().contains(LABEL); + protected void checkDuplicatedBranches(CheckContext ctx, Tree tree, List branches) { + // handled by S1871 } @Override - protected boolean shouldIgnore(Tree tree) { - return tree instanceof NativeTree && - ((NativeTree) tree).nativeKind().toString().equals(SEMICOLON); + protected void onAllIdenticalBranches(CheckContext ctx, Tree tree) { + ctx.reportIssue(tree, "Remove this conditional structure or edit its code blocks so that they're not all the same."); } + } diff --git a/sonar-go-checks/src/main/java/org/sonar/go/checks/BadFunctionNameCheck.java b/sonar-go-checks/src/main/java/org/sonar/go/checks/BadFunctionNameCheck.java new file mode 100644 index 00000000..9c449120 --- /dev/null +++ b/sonar-go-checks/src/main/java/org/sonar/go/checks/BadFunctionNameCheck.java @@ -0,0 +1,50 @@ +/* + * SonarSource Go + * Copyright (C) 2018-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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.go.checks; + +import java.util.regex.Pattern; +import org.sonar.check.Rule; +import org.sonar.check.RuleProperty; +import org.sonarsource.slang.api.FunctionDeclarationTree; +import org.sonarsource.slang.api.IdentifierTree; +import org.sonarsource.slang.checks.api.InitContext; +import org.sonarsource.slang.checks.api.SlangCheck; + +@Rule(key = "S100") +public class BadFunctionNameCheck implements SlangCheck { + + @RuleProperty( + key = "format", + description = "Regular expression used to check the function names against.", + defaultValue = GoChecksConstants.GO_NAMING_DEFAULT) + public String format = GoChecksConstants.GO_NAMING_DEFAULT; + + private String message(String name) { + return "Rename function \"" + name + "\" to match the regular expression " + format; + } + + @Override + public void initialize(InitContext init) { + Pattern pattern = Pattern.compile(format); + init.register(FunctionDeclarationTree.class, (ctx, fnDeclarationTree) -> { + IdentifierTree name = fnDeclarationTree.name(); + if (!fnDeclarationTree.isConstructor() && name != null && !pattern.matcher(name.name()).matches()) { + ctx.reportIssue(fnDeclarationTree.name(), message(name.name())); + } + }); + } +} diff --git a/sonar-go-checks/src/main/java/org/sonar/go/checks/BooleanInversionCheck.java b/sonar-go-checks/src/main/java/org/sonar/go/checks/BooleanInversionCheck.java new file mode 100644 index 00000000..c37b304e --- /dev/null +++ b/sonar-go-checks/src/main/java/org/sonar/go/checks/BooleanInversionCheck.java @@ -0,0 +1,61 @@ +/* + * SonarSource Go + * Copyright (C) 2018-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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.go.checks; + +import java.util.EnumMap; +import java.util.Map; +import org.sonar.check.Rule; +import org.sonarsource.slang.api.BinaryExpressionTree; +import org.sonarsource.slang.api.BinaryExpressionTree.Operator; +import org.sonarsource.slang.api.Tree; +import org.sonarsource.slang.api.UnaryExpressionTree; +import org.sonarsource.slang.checks.api.InitContext; +import org.sonarsource.slang.checks.api.SlangCheck; + +import static org.sonar.go.checks.utils.ExpressionUtils.skipParentheses; + +@Rule(key = "S1940") +public class BooleanInversionCheck implements SlangCheck { + + private static final Map OPERATORS = createOperatorsMap(); + + private static Map createOperatorsMap() { + Map operatorsMap = new EnumMap<>(Operator.class); + operatorsMap.put(Operator.EQUAL_TO, "!="); + operatorsMap.put(Operator.NOT_EQUAL_TO, "=="); + operatorsMap.put(Operator.LESS_THAN, ">="); + operatorsMap.put(Operator.GREATER_THAN, "<="); + operatorsMap.put(Operator.LESS_THAN_OR_EQUAL_TO, ">"); + operatorsMap.put(Operator.GREATER_THAN_OR_EQUAL_TO, "<"); + return operatorsMap; + } + + @Override + public void initialize(InitContext init) { + init.register(UnaryExpressionTree.class, (ctx, tree) -> { + Tree innerExpression = skipParentheses(tree.operand()); + if (tree.operator() == UnaryExpressionTree.Operator.NEGATE && innerExpression instanceof BinaryExpressionTree binaryExpression) { + String oppositeOperator = OPERATORS.get(binaryExpression.operator()); + if (oppositeOperator != null) { + String message = String.format("Use the opposite operator (\"%s\") instead.", oppositeOperator); + ctx.reportIssue(tree, message); + } + } + }); + } + +} diff --git a/sonar-go-checks/src/main/java/org/sonar/go/checks/BooleanLiteralCheck.java b/sonar-go-checks/src/main/java/org/sonar/go/checks/BooleanLiteralCheck.java new file mode 100644 index 00000000..960602b3 --- /dev/null +++ b/sonar-go-checks/src/main/java/org/sonar/go/checks/BooleanLiteralCheck.java @@ -0,0 +1,81 @@ +/* + * SonarSource Go + * Copyright (C) 2018-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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.go.checks; + +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import javax.annotation.Nullable; +import org.sonar.check.Rule; +import org.sonar.go.checks.utils.ExpressionUtils; +import org.sonarsource.slang.api.BinaryExpressionTree; +import org.sonarsource.slang.api.BlockTree; +import org.sonarsource.slang.api.IfTree; +import org.sonarsource.slang.api.Tree; +import org.sonarsource.slang.api.UnaryExpressionTree; +import org.sonarsource.slang.checks.api.InitContext; +import org.sonarsource.slang.checks.api.SlangCheck; + +@Rule(key = "S1125") +public class BooleanLiteralCheck implements SlangCheck { + private static final List CONDITIONAL_BINARY_OPERATORS = Arrays.asList( + BinaryExpressionTree.Operator.CONDITIONAL_AND, + BinaryExpressionTree.Operator.CONDITIONAL_OR); + + private static final String MESSAGE = "Remove the unnecessary Boolean literal."; + + @Override + public void initialize(InitContext init) { + init.register(IfTree.class, (ctx, ifTree) -> { + if (isIfWithMaxTwoBranches(ctx.parent(), ifTree) && !hasBlockBranch(ifTree)) { + getBooleanLiteral(ifTree.thenBranch(), ifTree.elseBranch()) + .ifPresent(booleanLiteral -> ctx.reportIssue(booleanLiteral, MESSAGE)); + } + }); + + init.register(BinaryExpressionTree.class, (ctx, binaryExprTree) -> { + if (CONDITIONAL_BINARY_OPERATORS.contains(binaryExprTree.operator())) { + getBooleanLiteral(binaryExprTree.leftOperand(), binaryExprTree.rightOperand()) + .ifPresent(booleanLiteral -> ctx.reportIssue(booleanLiteral, MESSAGE)); + } + }); + + init.register(UnaryExpressionTree.class, (ctx, unaryExprTree) -> { + if (UnaryExpressionTree.Operator.NEGATE.equals(unaryExprTree.operator())) { + getBooleanLiteral(unaryExprTree.operand()) + .ifPresent(booleanLiteral -> ctx.reportIssue(booleanLiteral, MESSAGE)); + } + }); + } + + private static boolean isIfWithMaxTwoBranches(@Nullable Tree parent, IfTree ifTree) { + boolean isElseIf = parent instanceof IfTree parentIfTree && parentIfTree.elseBranch() == ifTree; + boolean isIfElseIf = ifTree.elseBranch() instanceof IfTree; + return !isElseIf && !isIfElseIf; + } + + private static boolean hasBlockBranch(IfTree ifTree) { + return ifTree.thenBranch() instanceof BlockTree || ifTree.elseBranch() instanceof BlockTree; + } + + private static Optional getBooleanLiteral(Tree... trees) { + return Arrays.stream(trees) + .map(ExpressionUtils::skipParentheses) + .filter(ExpressionUtils::isBooleanLiteral) + .findFirst(); + } +} diff --git a/sonar-go-checks/src/main/java/org/sonar/go/checks/CodeAfterJumpGoCheck.java b/sonar-go-checks/src/main/java/org/sonar/go/checks/CodeAfterJumpGoCheck.java new file mode 100644 index 00000000..f87d416f --- /dev/null +++ b/sonar-go-checks/src/main/java/org/sonar/go/checks/CodeAfterJumpGoCheck.java @@ -0,0 +1,81 @@ +/* + * SonarSource Go + * Copyright (C) 2018-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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.go.checks; + +import java.util.List; +import org.sonar.check.Rule; +import org.sonarsource.slang.api.BlockTree; +import org.sonarsource.slang.api.HasKeyword; +import org.sonarsource.slang.api.JumpTree; +import org.sonarsource.slang.api.NativeTree; +import org.sonarsource.slang.api.ReturnTree; +import org.sonarsource.slang.api.ThrowTree; +import org.sonarsource.slang.api.Tree; +import org.sonarsource.slang.checks.api.CheckContext; +import org.sonarsource.slang.checks.api.InitContext; +import org.sonarsource.slang.checks.api.SlangCheck; + +import static org.sonar.go.checks.NativeKinds.LABEL; +import static org.sonar.go.checks.NativeKinds.SEMICOLON; + +@Rule(key = "S1763") +public class CodeAfterJumpGoCheck implements SlangCheck { + private static final String MESSAGE = "Refactor this piece of code to not have any dead code after this \"%s\"."; + + @Override + public void initialize(InitContext init) { + init.register(BlockTree.class, (ctx, blockTree) -> checkStatements(ctx, blockTree.statementOrExpressions())); + } + + private static void checkStatements(CheckContext ctx, List statementsOrExpressions) { + if (statementsOrExpressions.size() < 2) { + return; + } + + int index = 0; + while (index < statementsOrExpressions.size() - 1) { + Tree current = statementsOrExpressions.get(index); + index++; + + Tree next = statementsOrExpressions.get(index); + while (index < statementsOrExpressions.size() && shouldIgnore(next)) { + next = statementsOrExpressions.get(index); + index++; + } + + if (isJump(current) && + !shouldIgnore(next) && + !isValidAfterJump(next)) { + ctx.reportIssue(current, String.format(MESSAGE, ((HasKeyword) current).keyword().text())); + } + } + } + + private static boolean isJump(Tree tree) { + return tree instanceof JumpTree || tree instanceof ReturnTree || tree instanceof ThrowTree; + } + + private static boolean isValidAfterJump(Tree tree) { + return tree instanceof NativeTree nativeTree && + nativeTree.nativeKind().toString().contains(LABEL); + } + + private static boolean shouldIgnore(Tree tree) { + return tree instanceof NativeTree nativeTree && + nativeTree.nativeKind().toString().equals(SEMICOLON); + } +} diff --git a/sonar-go-plugin/src/main/java/org/sonar/go/checks/DuplicateBranchGoCheck.java b/sonar-go-checks/src/main/java/org/sonar/go/checks/DuplicateBranchGoCheck.java similarity index 51% rename from sonar-go-plugin/src/main/java/org/sonar/go/checks/DuplicateBranchGoCheck.java rename to sonar-go-checks/src/main/java/org/sonar/go/checks/DuplicateBranchGoCheck.java index 27b11b91..0f7c84c0 100644 --- a/sonar-go-plugin/src/main/java/org/sonar/go/checks/DuplicateBranchGoCheck.java +++ b/sonar-go-checks/src/main/java/org/sonar/go/checks/DuplicateBranchGoCheck.java @@ -18,22 +18,66 @@ import java.util.List; import java.util.stream.Collectors; +import javax.annotation.Nullable; import org.sonar.check.Rule; +import org.sonarsource.slang.api.BlockTree; import org.sonarsource.slang.api.MatchTree; +import org.sonarsource.slang.api.TextRange; import org.sonarsource.slang.api.Token; import org.sonarsource.slang.api.Tree; -import org.sonarsource.slang.checks.DuplicateBranchCheck; import org.sonarsource.slang.checks.api.CheckContext; +import org.sonarsource.slang.checks.api.SecondaryLocation; +import org.sonarsource.slang.utils.SyntacticEquivalence; @Rule(key = "S1871") -public class DuplicateBranchGoCheck extends DuplicateBranchCheck { +public class DuplicateBranchGoCheck extends AbstractBranchDuplicationCheck { + + @Override + protected void checkDuplicatedBranches(CheckContext ctx, Tree tree, List branches) { + for (List group : SyntacticEquivalence.findDuplicatedGroups(branches)) { + Tree original = group.get(0); + group.stream().skip(1) + .filter(DuplicateBranchGoCheck::spansMultipleLines) + .forEach(duplicated -> { + TextRange originalRange = original.metaData().textRange(); + ctx.reportIssue( + duplicated, + "This branch's code block is the same as the block for the branch on line " + originalRange.start().line() + ".", + new SecondaryLocation(originalRange, "Original")); + }); + } + + } + + @Override + protected void onAllIdenticalBranches(CheckContext ctx, Tree tree) { + // handled by S3923 + } + + protected static boolean spansMultipleLines(@Nullable Tree tree) { + if (tree == null) { + return false; + } + if (tree instanceof BlockTree block) { + List statements = block.statementOrExpressions(); + if (statements.isEmpty()) { + return false; + } + Tree firstStatement = statements.get(0); + Tree lastStatement = statements.get(statements.size() - 1); + return firstStatement.metaData().textRange().start().line() != lastStatement.metaData().textRange().end().line(); + } + TextRange range = tree.metaData().textRange(); + return range.start().line() < range.end().line(); + } + @Override protected void checkConditionalStructure(CheckContext ctx, Tree tree, ConditionalStructure conditional) { /* * If we enter a type switch, we may find branches with similar ASTs but different semantics. * In this case, we stop exploring the conditional structure to avoid raising FPs. */ - if (tree instanceof MatchTree && isTypeSwitch((MatchTree) tree)) { + if (tree instanceof MatchTree matchTree && isTypeSwitch(matchTree)) { return; } super.checkConditionalStructure(ctx, tree, conditional); diff --git a/sonar-go-checks/src/main/java/org/sonar/go/checks/DuplicatedFunctionImplementationCheck.java b/sonar-go-checks/src/main/java/org/sonar/go/checks/DuplicatedFunctionImplementationCheck.java new file mode 100644 index 00000000..534e53ad --- /dev/null +++ b/sonar-go-checks/src/main/java/org/sonar/go/checks/DuplicatedFunctionImplementationCheck.java @@ -0,0 +1,117 @@ +/* + * SonarSource Go + * Copyright (C) 2018-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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.go.checks; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.IntStream; +import org.sonar.check.Rule; +import org.sonarsource.slang.api.BlockTree; +import org.sonarsource.slang.api.FunctionDeclarationTree; +import org.sonarsource.slang.api.IdentifierTree; +import org.sonarsource.slang.api.TopLevelTree; +import org.sonarsource.slang.api.Tree; +import org.sonarsource.slang.checks.api.CheckContext; +import org.sonarsource.slang.checks.api.InitContext; +import org.sonarsource.slang.checks.api.SecondaryLocation; +import org.sonarsource.slang.checks.api.SlangCheck; +import org.sonarsource.slang.visitors.TreeContext; +import org.sonarsource.slang.visitors.TreeVisitor; + +import static org.sonarsource.slang.utils.SyntacticEquivalence.areEquivalent; + +@Rule(key = "S4144") +public class DuplicatedFunctionImplementationCheck implements SlangCheck { + + private static final String MESSAGE = "Update this function so that its implementation is not identical to \"%s\" on line %s."; + private static final String MESSAGE_NO_NAME = "Update this function so that its implementation is not identical to the one on line %s."; + private static final int MINIMUM_STATEMENTS_COUNT = 2; + + @Override + public void initialize(InitContext init) { + init.register(TopLevelTree.class, (ctx, tree) -> { + Map> functionsByParents = new HashMap<>(); + TreeVisitor functionVisitor = new TreeVisitor<>(); + functionVisitor.register(FunctionDeclarationTree.class, (functionCtx, functionDeclarationTree) -> { + if (!functionDeclarationTree.isConstructor()) { + functionsByParents + .computeIfAbsent(functionCtx.ancestors().peek(), key -> new ArrayList<>()) + .add(functionDeclarationTree); + } + }); + functionVisitor.scan(new TreeContext(), tree); + + for (Map.Entry> entry : functionsByParents.entrySet()) { + check(ctx, entry.getValue()); + } + }); + } + + private static void check(CheckContext ctx, List functionDeclarations) { + Set reportedDuplicates = new HashSet<>(); + IntStream.range(0, functionDeclarations.size()).forEach(i -> { + FunctionDeclarationTree original = functionDeclarations.get(i); + functionDeclarations.stream() + .skip(i + 1L) + .filter(f -> !reportedDuplicates.contains(f)) + .filter(DuplicatedFunctionImplementationCheck::hasMinimumSize) + .filter(f -> areDuplicatedImplementation(original, f)) + .forEach(duplicate -> { + reportDuplicate(ctx, original, duplicate); + reportedDuplicates.add(duplicate); + }); + }); + + } + + private static boolean hasMinimumSize(FunctionDeclarationTree function) { + BlockTree functionBody = function.body(); + if (functionBody == null) { + return false; + } + return functionBody.statementOrExpressions().size() >= MINIMUM_STATEMENTS_COUNT; + } + + private static boolean areDuplicatedImplementation(FunctionDeclarationTree original, FunctionDeclarationTree possibleDuplicate) { + return areEquivalent(original.nativeChildren(), possibleDuplicate.nativeChildren()) + && areEquivalent(original.formalParameters(), possibleDuplicate.formalParameters()) + && areEquivalent(original.body(), possibleDuplicate.body()); + } + + private static void reportDuplicate(CheckContext ctx, FunctionDeclarationTree original, FunctionDeclarationTree duplicate) { + IdentifierTree identifier = original.name(); + int line = original.metaData().textRange().start().line(); + String message; + Tree secondaryTree; + if (identifier != null) { + secondaryTree = identifier; + message = String.format(MESSAGE, identifier.name(), line); + } else { + secondaryTree = original; + message = String.format(MESSAGE_NO_NAME, line); + } + SecondaryLocation secondaryLocation = new SecondaryLocation(secondaryTree, "original implementation"); + IdentifierTree duplicateIdentifier = duplicate.name(); + Tree primaryTree = duplicateIdentifier != null ? duplicateIdentifier : duplicate; + ctx.reportIssue(primaryTree, message, secondaryLocation); + } + +} diff --git a/sonar-go-checks/src/main/java/org/sonar/go/checks/ElseIfWithoutElseCheck.java b/sonar-go-checks/src/main/java/org/sonar/go/checks/ElseIfWithoutElseCheck.java new file mode 100644 index 00000000..b8d32d34 --- /dev/null +++ b/sonar-go-checks/src/main/java/org/sonar/go/checks/ElseIfWithoutElseCheck.java @@ -0,0 +1,95 @@ +/* + * SonarSource Go + * Copyright (C) 2018-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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.go.checks; + +import java.util.List; +import org.sonar.check.Rule; +import org.sonarsource.slang.api.BlockTree; +import org.sonarsource.slang.api.IfTree; +import org.sonarsource.slang.api.JumpTree; +import org.sonarsource.slang.api.ReturnTree; +import org.sonarsource.slang.api.TextRange; +import org.sonarsource.slang.api.ThrowTree; +import org.sonarsource.slang.api.Token; +import org.sonarsource.slang.api.Tree; +import org.sonarsource.slang.checks.api.CheckContext; +import org.sonarsource.slang.checks.api.InitContext; +import org.sonarsource.slang.checks.api.SlangCheck; +import org.sonarsource.slang.impl.TextRangeImpl; + +@Rule(key = "S126") +public class ElseIfWithoutElseCheck implements SlangCheck { + + private static final String MESSAGE = "Add the missing \"else\" clause."; + + @Override + public void initialize(InitContext init) { + init.register(IfTree.class, (ctx, ifTree) -> { + if (ifTree.elseBranch() == null || !isTopLevelIf(ctx, ifTree)) { + return; + } + + IfTree prevTree = ifTree; + boolean endsWithReturn = endsWithReturnBreakOrThrow(ifTree); + while (ifTree.elseBranch() instanceof IfTree) { + prevTree = ifTree; + ifTree = (IfTree) (ifTree.elseBranch()); + endsWithReturn = endsWithReturn && endsWithReturnBreakOrThrow(ifTree); + } + + // We raise an issue if + // - at least one branch does not finish with return/break/throw + // - no "else" is defined + if (!endsWithReturn && ifTree.elseBranch() == null) { + Token elseToken = prevTree.elseKeyword(); + Token ifToken = ifTree.ifKeyword(); + TextRange textRange = new TextRangeImpl( + elseToken.textRange().start(), + ifToken.textRange().end()); + ctx.reportIssue(textRange, MESSAGE); + } + + }); + } + + private static boolean isTopLevelIf(CheckContext ctx, IfTree ifTree) { + Tree firstAncestor = ctx.ancestors().getFirst(); + if (firstAncestor instanceof IfTree ifTreeAncestor) { + // if ifTree is different from the else branch of firstAncestor, it means that ifTree is a statement inside + // firstAncestor and so ifTree is the top level "if" + return ifTreeAncestor.elseBranch() != ifTree; + } + return true; + } + + private static boolean endsWithReturnBreakOrThrow(IfTree ifTree) { + Tree thenBranch = ifTree.thenBranch(); + if (thenBranch instanceof BlockTree blockTree) { + List statements = blockTree.statementOrExpressions(); + if (!statements.isEmpty()) { + Tree lastStmt = statements.get(statements.size() - 1); + return isReturnBreakOrThrow(lastStmt); + } + } + // Curly braces can be omitted when there is only one statement inside the "if" + return isReturnBreakOrThrow(thenBranch); + } + + private static boolean isReturnBreakOrThrow(Tree tree) { + return tree instanceof JumpTree || tree instanceof ReturnTree || tree instanceof ThrowTree; + } +} diff --git a/sonar-go-checks/src/main/java/org/sonar/go/checks/EmptyBlockCheck.java b/sonar-go-checks/src/main/java/org/sonar/go/checks/EmptyBlockCheck.java new file mode 100644 index 00000000..dc841140 --- /dev/null +++ b/sonar-go-checks/src/main/java/org/sonar/go/checks/EmptyBlockCheck.java @@ -0,0 +1,68 @@ +/* + * SonarSource Go + * Copyright (C) 2018-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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.go.checks; + +import javax.annotation.Nullable; +import org.sonar.check.Rule; +import org.sonarsource.slang.api.BlockTree; +import org.sonarsource.slang.api.FunctionDeclarationTree; +import org.sonarsource.slang.api.LoopTree; +import org.sonarsource.slang.api.MatchTree; +import org.sonarsource.slang.api.NativeTree; +import org.sonarsource.slang.api.Tree; +import org.sonarsource.slang.checks.api.CheckContext; +import org.sonarsource.slang.checks.api.InitContext; +import org.sonarsource.slang.checks.api.SlangCheck; + +@Rule(key = "S108") +public class EmptyBlockCheck implements SlangCheck { + + private static final String MESSAGE = "Either remove or fill this block of code."; + + @Override + public void initialize(InitContext init) { + init.register(BlockTree.class, (ctx, blockTree) -> { + Tree parent = ctx.parent(); + if (isValidBlock(parent) && blockTree.statementOrExpressions().isEmpty()) { + checkComments(ctx, blockTree); + } + }); + + init.register(MatchTree.class, (ctx, matchTree) -> { + if (matchTree.cases().isEmpty()) { + checkComments(ctx, matchTree); + } + }); + } + + private static boolean isValidBlock(@Nullable Tree parent) { + return !(parent instanceof FunctionDeclarationTree) + && !(parent instanceof NativeTree) + && !isWhileLoop(parent); + } + + private static boolean isWhileLoop(@Nullable Tree parent) { + return parent instanceof LoopTree loopTree && loopTree.kind() == LoopTree.LoopKind.WHILE; + } + + private static void checkComments(CheckContext ctx, Tree tree) { + if (tree.metaData().commentsInside().isEmpty()) { + ctx.reportIssue(tree, MESSAGE); + } + } + +} diff --git a/sonar-go-checks/src/main/java/org/sonar/go/checks/EmptyCommentCheck.java b/sonar-go-checks/src/main/java/org/sonar/go/checks/EmptyCommentCheck.java new file mode 100644 index 00000000..7cbdf88b --- /dev/null +++ b/sonar-go-checks/src/main/java/org/sonar/go/checks/EmptyCommentCheck.java @@ -0,0 +1,34 @@ +/* + * SonarSource Go + * Copyright (C) 2018-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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.go.checks; + +import org.sonar.check.Rule; +import org.sonarsource.slang.api.TopLevelTree; +import org.sonarsource.slang.checks.api.InitContext; +import org.sonarsource.slang.checks.api.SlangCheck; + +@Rule(key = "S4663") +public class EmptyCommentCheck implements SlangCheck { + + @Override + public void initialize(InitContext init) { + init.register(TopLevelTree.class, (ctx, tree) -> tree.allComments().stream() + .filter(comment -> comment.contentText().trim().isEmpty() && !comment.contentRange().end().equals(comment.textRange().end())) + .forEach(comment -> ctx.reportIssue(comment, "Remove this comment, it is empty."))); + } + +} diff --git a/sonar-go-checks/src/main/java/org/sonar/go/checks/EmptyFunctionCheck.java b/sonar-go-checks/src/main/java/org/sonar/go/checks/EmptyFunctionCheck.java new file mode 100644 index 00000000..8d57a6d8 --- /dev/null +++ b/sonar-go-checks/src/main/java/org/sonar/go/checks/EmptyFunctionCheck.java @@ -0,0 +1,49 @@ +/* + * SonarSource Go + * Copyright (C) 2018-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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.go.checks; + +import org.sonar.check.Rule; +import org.sonarsource.slang.api.BlockTree; +import org.sonarsource.slang.api.FunctionDeclarationTree; +import org.sonarsource.slang.api.TreeMetaData; +import org.sonarsource.slang.checks.api.InitContext; +import org.sonarsource.slang.checks.api.SlangCheck; + +@Rule(key = "S1186") +public class EmptyFunctionCheck implements SlangCheck { + + @Override + public void initialize(InitContext init) { + init.register(FunctionDeclarationTree.class, (ctx, tree) -> { + BlockTree body = tree.body(); + if (!tree.isConstructor() && body != null && body.statementOrExpressions().isEmpty() && !hasComment(body, ctx.parent().metaData())) { + ctx.reportIssue(body, "Add a nested comment explaining why this function is empty or complete the implementation."); + } + }); + } + + private static boolean hasComment(BlockTree body, TreeMetaData parentMetaData) { + if (!body.metaData().commentsInside().isEmpty()) { + return true; + } + + int emptyBodyEndLine = body.textRange().end().line(); + return parentMetaData.commentsInside().stream() + .anyMatch(comment -> comment.contentRange().start().line() == emptyBodyEndLine); + } + +} diff --git a/sonar-go-checks/src/main/java/org/sonar/go/checks/FileHeaderCheck.java b/sonar-go-checks/src/main/java/org/sonar/go/checks/FileHeaderCheck.java new file mode 100644 index 00000000..3a027b1d --- /dev/null +++ b/sonar-go-checks/src/main/java/org/sonar/go/checks/FileHeaderCheck.java @@ -0,0 +1,103 @@ +/* + * SonarSource Go + * Copyright (C) 2018-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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.go.checks; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.IntStream; +import org.sonar.check.Rule; +import org.sonar.check.RuleProperty; +import org.sonarsource.slang.api.TopLevelTree; +import org.sonarsource.slang.checks.api.CheckContext; +import org.sonarsource.slang.checks.api.InitContext; +import org.sonarsource.slang.checks.api.SlangCheck; + +@Rule(key = "S1451") +public class FileHeaderCheck implements SlangCheck { + + private static final String MESSAGE = "Add or update the header of this file."; + private static final String DEFAULT_HEADER_FORMAT = ""; + + @RuleProperty( + key = "headerFormat", + description = "Expected copyright and license header", + defaultValue = DEFAULT_HEADER_FORMAT, + type = "TEXT") + public String headerFormat = DEFAULT_HEADER_FORMAT; + + @RuleProperty( + key = "isRegularExpression", + description = "Whether the headerFormat is a regular expression", + defaultValue = "false") + public boolean isRegularExpression = false; + private Pattern searchPattern = null; + private String[] expectedLines = null; + + private static final String LINES_REGEX = "\r\n|\n|\r"; + + @Override + public void initialize(InitContext init) { + initializeParameters(); + init.register(TopLevelTree.class, (ctx, tree) -> { + if (isRegularExpression) { + checkRegularExpression(ctx); + } else { + checkExpectedLines(ctx); + } + }); + } + + private void initializeParameters() { + if (isRegularExpression) { + try { + searchPattern = Pattern.compile(getHeaderFormat(), Pattern.DOTALL); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("[" + getClass().getSimpleName() + "] Unable to compile the regular expression: " + headerFormat, e); + } + } else { + expectedLines = headerFormat.split(LINES_REGEX); + } + } + + private void checkExpectedLines(CheckContext ctx) { + String[] lines = ctx.fileContent().split(LINES_REGEX, -1); + if (lines.length < expectedLines.length) { + ctx.reportFileIssue(MESSAGE); + } else { + IntStream.range(0, expectedLines.length) + .filter(lineIndex -> !lines[lineIndex].equals(expectedLines[lineIndex])) + .findFirst() + .ifPresent(lineIndex -> ctx.reportFileIssue(MESSAGE)); + } + } + + private void checkRegularExpression(CheckContext ctx) { + Matcher matcher = searchPattern.matcher(ctx.fileContent()); + if (!matcher.find()) { + ctx.reportFileIssue(MESSAGE); + } + } + + private String getHeaderFormat() { + String format = headerFormat; + if (format.charAt(0) != '^') { + format = "^" + format; + } + return format; + } + +} diff --git a/sonar-go-checks/src/main/java/org/sonar/go/checks/FixMeCommentCheck.java b/sonar-go-checks/src/main/java/org/sonar/go/checks/FixMeCommentCheck.java new file mode 100644 index 00000000..d9c7ffb6 --- /dev/null +++ b/sonar-go-checks/src/main/java/org/sonar/go/checks/FixMeCommentCheck.java @@ -0,0 +1,54 @@ +/* + * SonarSource Go + * Copyright (C) 2018-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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.go.checks; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.sonar.check.Rule; +import org.sonarsource.analyzer.commons.TokenLocation; +import org.sonarsource.slang.api.TextPointer; +import org.sonarsource.slang.api.TextRange; +import org.sonarsource.slang.api.TopLevelTree; +import org.sonarsource.slang.checks.api.InitContext; +import org.sonarsource.slang.checks.api.SlangCheck; +import org.sonarsource.slang.impl.TextPointerImpl; +import org.sonarsource.slang.impl.TextRangeImpl; + +@Rule(key = "S1134") +public class FixMeCommentCheck implements SlangCheck { + + private final Pattern fixMePattern = Pattern.compile("(?i)(^|[[^\\p{L}]&&\\D])(fixme)($|[[^\\p{L}]&&\\D])"); + + @Override + public void initialize(InitContext init) { + init.register(TopLevelTree.class, (ctx, tree) -> tree.allComments().forEach(comment -> { + Matcher matcher = fixMePattern.matcher(comment.text()); + if (matcher.find()) { + TextPointer start = comment.textRange().start(); + TokenLocation location = new TokenLocation( + start.line(), + start.lineOffset(), + comment.text().substring(0, matcher.start(2))); + TextRange fixMeRange = new TextRangeImpl( + new TextPointerImpl(location.endLine(), location.endLineOffset()), + new TextPointerImpl(location.endLine(), location.endLineOffset() + 5)); + ctx.reportIssue(fixMeRange, "Take the required action to fix the issue indicated by this \"FIXME\" comment."); + } + })); + } + +} diff --git a/sonar-go-checks/src/main/java/org/sonar/go/checks/FunctionCognitiveComplexityCheck.java b/sonar-go-checks/src/main/java/org/sonar/go/checks/FunctionCognitiveComplexityCheck.java new file mode 100644 index 00000000..6acad096 --- /dev/null +++ b/sonar-go-checks/src/main/java/org/sonar/go/checks/FunctionCognitiveComplexityCheck.java @@ -0,0 +1,70 @@ +/* + * SonarSource Go + * Copyright (C) 2018-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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.go.checks; + +import java.util.List; +import org.sonar.check.Rule; +import org.sonar.check.RuleProperty; +import org.sonar.go.checks.complexity.CognitiveComplexity; +import org.sonarsource.slang.api.FunctionDeclarationTree; +import org.sonarsource.slang.checks.api.InitContext; +import org.sonarsource.slang.checks.api.SecondaryLocation; +import org.sonarsource.slang.checks.api.SlangCheck; + +@Rule(key = "S3776") +public class FunctionCognitiveComplexityCheck implements SlangCheck { + + private static final int DEFAULT_THRESHOLD = 15; + + @RuleProperty( + key = "threshold", + description = "The maximum authorized complexity.", + defaultValue = "" + DEFAULT_THRESHOLD) + public int threshold = DEFAULT_THRESHOLD; + + @Override + public void initialize(InitContext init) { + init.register(FunctionDeclarationTree.class, (ctx, tree) -> { + if (tree.name() == null) { + return; + } + + CognitiveComplexity complexity = new CognitiveComplexity(tree); + if (complexity.value() > threshold) { + String message = String.format( + "Refactor this method to reduce its Cognitive Complexity from %s to the %s allowed.", + complexity.value(), + threshold); + List secondaryLocations = complexity.increments().stream() + .map(FunctionCognitiveComplexityCheck::secondaryLocation) + .toList(); + Double gap = (double) complexity.value() - threshold; + ctx.reportIssue(tree::rangeToHighlight, message, secondaryLocations, gap); + } + }); + } + + private static SecondaryLocation secondaryLocation(CognitiveComplexity.Increment increment) { + int nestingLevel = increment.nestingLevel(); + String message = "+" + (nestingLevel + 1); + if (nestingLevel > 0) { + message += " (incl " + nestingLevel + " for nesting)"; + } + return new SecondaryLocation(increment.token().textRange(), message); + } + +} diff --git a/sonar-go-checks/src/main/java/org/sonar/go/checks/GoCheckList.java b/sonar-go-checks/src/main/java/org/sonar/go/checks/GoCheckList.java new file mode 100644 index 00000000..cc5f5996 --- /dev/null +++ b/sonar-go-checks/src/main/java/org/sonar/go/checks/GoCheckList.java @@ -0,0 +1,70 @@ +/* + * SonarSource Go + * Copyright (C) 2018-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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.go.checks; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class GoCheckList { + + private GoCheckList() { + // utility class + } + + public static List> checks() { + return new ArrayList<>(Arrays.>asList( + AllBranchesIdenticalCheck.class, + BadFunctionNameCheck.class, + BooleanInversionCheck.class, + BooleanLiteralCheck.class, + CodeAfterJumpGoCheck.class, + DuplicateBranchGoCheck.class, + DuplicatedFunctionImplementationCheck.class, + ElseIfWithoutElseCheck.class, + EmptyBlockCheck.class, + EmptyCommentCheck.class, + EmptyFunctionCheck.class, + FileHeaderCheck.class, + FixMeCommentCheck.class, + FunctionCognitiveComplexityCheck.class, + HardcodedCredentialsCheck.class, + HardcodedIpCheck.class, + IdenticalBinaryOperandCheck.class, + IdenticalConditionsCheck.class, + IfConditionalAlwaysTrueOrFalseCheck.class, + MatchCaseTooBigCheck.class, + MatchWithoutElseCheck.class, + NestedMatchCheck.class, + OctalValuesCheck.class, + OneStatementPerLineGoCheck.class, + ParsingErrorCheck.class, + RedundantParenthesesCheck.class, + SelfAssignmentCheck.class, + StringLiteralDuplicatedCheck.class, + TodoCommentCheck.class, + TooComplexExpressionCheck.class, + TooDeeplyNestedStatementsCheck.class, + TooLongFunctionCheck.class, + TooLongLineCheck.class, + TooManyCasesCheck.class, + TooManyLinesOfCodeFileCheck.class, + TooManyParametersCheck.class, + VariableAndParameterNameCheck.class, + WrongAssignmentOperatorCheck.class)); + } +} diff --git a/sonar-go-checks/src/main/java/org/sonar/go/checks/GoChecksConstants.java b/sonar-go-checks/src/main/java/org/sonar/go/checks/GoChecksConstants.java new file mode 100644 index 00000000..e6c3d121 --- /dev/null +++ b/sonar-go-checks/src/main/java/org/sonar/go/checks/GoChecksConstants.java @@ -0,0 +1,26 @@ +/* + * SonarSource Go + * Copyright (C) 2018-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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.go.checks; + +public final class GoChecksConstants { + + public static final String GO_NAMING_DEFAULT = "^(_|[a-zA-Z0-9]+)$"; + + private GoChecksConstants() { + // empty + } +} diff --git a/sonar-go-checks/src/main/java/org/sonar/go/checks/HardcodedCredentialsCheck.java b/sonar-go-checks/src/main/java/org/sonar/go/checks/HardcodedCredentialsCheck.java new file mode 100644 index 00000000..0bae1106 --- /dev/null +++ b/sonar-go-checks/src/main/java/org/sonar/go/checks/HardcodedCredentialsCheck.java @@ -0,0 +1,148 @@ +/* + * SonarSource Go + * Copyright (C) 2018-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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.go.checks; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Stream; +import javax.annotation.Nullable; +import org.sonar.check.Rule; +import org.sonar.check.RuleProperty; +import org.sonar.go.checks.utils.ExpressionUtils; +import org.sonarsource.slang.api.AssignmentExpressionTree; +import org.sonarsource.slang.api.StringLiteralTree; +import org.sonarsource.slang.api.Tree; +import org.sonarsource.slang.api.VariableDeclarationTree; +import org.sonarsource.slang.checks.api.CheckContext; +import org.sonarsource.slang.checks.api.InitContext; +import org.sonarsource.slang.checks.api.SlangCheck; + +@Rule(key = "S2068") +public class HardcodedCredentialsCheck implements SlangCheck { + + private static final String DEFAULT_VALUE = "password,passwd,pwd,passphrase"; + private static final Pattern URI_PREFIX = Pattern.compile("^\\w{1,8}://"); + + @RuleProperty( + key = "credentialWords", + description = "Comma separated list of words identifying potential credentials", + defaultValue = DEFAULT_VALUE) + public String credentialWords = DEFAULT_VALUE; + + private List variablePatterns; + private List literalPatterns; + + @Override + public void initialize(InitContext init) { + init.register(AssignmentExpressionTree.class, (ctx, tree) -> { + Tree leftHandSide = tree.leftHandSide(); + ExpressionUtils.getMemberSelectOrIdentifierName(leftHandSide) + .ifPresent(variableName -> checkVariable(ctx, leftHandSide, variableName, tree.statementOrExpression())); + }); + + init.register(VariableDeclarationTree.class, (ctx, tree) -> checkVariable(ctx, tree.identifier(), tree.identifier().name(), tree.initializer())); + + init.register(StringLiteralTree.class, (ctx, tree) -> { + String content = tree.content(); + if (isURIWithCredentials(content)) { + ctx.reportIssue(tree, "Review this hard-coded URL, which may contain a credential."); + } else { + literalPatterns() + .map(pattern -> pattern.matcher(content)) + .filter(Matcher::find) + .map(matcher -> matcher.group(1)) + .filter(match -> !isQuery(content, match)) + .forEach(credential -> report(ctx, tree, credential)); + } + }); + } + + private static boolean isURIWithCredentials(String stringLiteral) { + if (URI_PREFIX.matcher(stringLiteral).find()) { + try { + String userInfo = new URI(stringLiteral).getUserInfo(); + if (userInfo != null) { + String[] parts = userInfo.split(":"); + return parts.length > 1 && !parts[0].equals(parts[1]); + } + } catch (URISyntaxException e) { + // ignore, stringLiteral is not a valid URI + } + } + return false; + } + + private static boolean isNotEmptyString(@Nullable Tree tree) { + return tree instanceof StringLiteralTree stringLiteralTree + && !stringLiteralTree.content().isEmpty(); + } + + private static boolean isQuery(String value, String match) { + String followingString = value.substring(value.indexOf(match) + match.length()); + return followingString.startsWith("=?") + || followingString.startsWith("=%") + || followingString.startsWith("=:") + // string format + || followingString.startsWith("={") + || followingString.equals("='"); + } + + private static void report(CheckContext ctx, Tree tree, String matchName) { + String message = String.format("\"%s\" detected here, make sure this is not a hard-coded credential.", matchName); + ctx.reportIssue(tree, message); + } + + private void checkVariable(CheckContext ctx, Tree variable, String variableName, @Nullable Tree value) { + if (isNotEmptyString(value)) { + variablePatterns() + .map(pattern -> pattern.matcher(variableName)) + .filter(Matcher::find) + .forEach(matcher -> checkAssignedValue(ctx, matcher, variable, ((StringLiteralTree) value).value())); + } + } + + private static void checkAssignedValue(CheckContext ctx, Matcher matcher, Tree leftHand, String value) { + if (!matcher.pattern().matcher(value).find()) { + report(ctx, leftHand, matcher.group(1)); + } + } + + private Stream variablePatterns() { + if (variablePatterns == null) { + variablePatterns = toPatterns(""); + } + return variablePatterns.stream(); + } + + private Stream literalPatterns() { + if (literalPatterns == null) { + literalPatterns = toPatterns("=\\S"); + } + return literalPatterns.stream(); + } + + private List toPatterns(String suffix) { + return Stream.of(credentialWords.split(",")) + .map(String::trim) + .map(word -> Pattern.compile("(" + word + ")" + suffix, Pattern.CASE_INSENSITIVE)) + .toList(); + } + +} diff --git a/sonar-go-checks/src/main/java/org/sonar/go/checks/HardcodedIpCheck.java b/sonar-go-checks/src/main/java/org/sonar/go/checks/HardcodedIpCheck.java new file mode 100644 index 00000000..5f0cedbc --- /dev/null +++ b/sonar-go-checks/src/main/java/org/sonar/go/checks/HardcodedIpCheck.java @@ -0,0 +1,126 @@ +/* + * SonarSource Go + * Copyright (C) 2018-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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.go.checks; + +import java.util.Arrays; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import javax.annotation.Nullable; +import org.sonar.check.Rule; +import org.sonarsource.slang.api.StringLiteralTree; +import org.sonarsource.slang.checks.api.InitContext; +import org.sonarsource.slang.checks.api.SlangCheck; + +@Rule(key = "S1313") +public class HardcodedIpCheck implements SlangCheck { + + private static final String IPV4_ALONE = "(?(?:\\d{1,3}\\.){3}\\d{1,3})"; + + private static final String IPV6_NO_PREFIX_COMPRESSION = "(\\p{XDigit}{1,4}::?){1,7}\\p{XDigit}{1,4}(::)?"; + private static final String IPV6_PREFIX_COMPRESSION = "::((\\p{XDigit}{1,4}:){0,6}\\p{XDigit}{1,4})?"; + private static final String IPV6_ALONE = ("(?(" + IPV6_NO_PREFIX_COMPRESSION + "|" + IPV6_PREFIX_COMPRESSION + ")??(:?" + IPV4_ALONE + ")?" + ")"); + private static final String IPV6_URL = "([^\\d.]*/)?\\[" + IPV6_ALONE + "]((:\\d{1,5})?(?![\\d.]))(/.*)?"; + + private static final Pattern IPV4_URL_REGEX = Pattern.compile("([^\\d.]*/)?" + IPV4_ALONE + "((:\\d{1,5})?(?![\\d.]))(/.*)?"); + private static final List IPV6_REGEX_LIST = Arrays.asList( + Pattern.compile(IPV6_ALONE), + Pattern.compile(IPV6_URL)); + + private static final Pattern IPV6_LOOPBACK = Pattern.compile("[0:]++0*+1"); + private static final Pattern IPV6_NON_ROUTABLE = Pattern.compile("[0:]++"); + private static final Pattern INVALID_IPV4_PART_PATTERN = Pattern.compile("^0\\d{1,2}"); + + private static final List IPV6_PREFIX_EXCEPTIONS = Arrays.asList("2001:db8:", "::ffff:0:127.", "::ffff:127."); + + private static final String MESSAGE = "Make sure using this hardcoded IP address is safe here."; + + @Override + public void initialize(InitContext init) { + init.register(StringLiteralTree.class, (ctx, tree) -> { + String content = tree.content(); + Matcher matcher = IPV4_URL_REGEX.matcher(content); + if (matcher.matches()) { + String ip = matcher.group("ipv4"); + if (isValidIPV4(ip) && !isIPV4Exception(ip)) { + ctx.reportIssue(tree, MESSAGE); + } + } else { + IPV6_REGEX_LIST.stream() + .map(pattern -> pattern.matcher(content)) + .filter(Matcher::matches) + .findFirst() + .filter(match -> { + String ipv6 = match.group("ipv6"); + String ipv4 = match.group("ipv4"); + return isValidIPV6(ipv6, ipv4) && !isIPV6Exception(ipv6); + }) + .ifPresent(match -> ctx.reportIssue(tree, MESSAGE)); + } + }); + } + + private static boolean isValidIPV4(String ip) { + String[] numbersAsStrings = ip.split("\\."); + return Arrays.stream(numbersAsStrings).noneMatch( + (INVALID_IPV4_PART_PATTERN.asPredicate()) + .or(value -> Integer.valueOf(value) > 255)); + } + + private static boolean isValidIPV6(String ipv6, @Nullable String ipv4) { + String[] split = ipv6.split("::?"); + int partCount = split.length; + int compressionSeparatorCount = getCompressionSeparatorCount(ipv6); + boolean validUncompressed; + boolean validCompressed; + if (ipv4 != null) { + boolean hasValidIPV4 = isValidIPV4(ipv4); + validUncompressed = hasValidIPV4 && compressionSeparatorCount == 0 && partCount == 7; + validCompressed = hasValidIPV4 && compressionSeparatorCount == 1 && partCount <= 6; + } else { + validUncompressed = compressionSeparatorCount == 0 && partCount == 8; + validCompressed = compressionSeparatorCount == 1 && partCount <= 7; + } + + return validUncompressed || validCompressed; + } + + private static boolean isIPV4Exception(String ip) { + return ip.startsWith("127.") + || ip.startsWith("2.5.") + || ip.startsWith("192.0.2.") + || ip.startsWith("198.51.100.") + || ip.startsWith("203.0.113.") + || "255.255.255.255".equals(ip) + || "0.0.0.0".equals(ip); + } + + private static boolean isIPV6Exception(String ip) { + return IPV6_PREFIX_EXCEPTIONS.stream().anyMatch(ip::startsWith) + || IPV6_LOOPBACK.matcher(ip).matches() + || IPV6_NON_ROUTABLE.matcher(ip).matches(); + } + + private static int getCompressionSeparatorCount(String str) { + int count = 0; + for (int i = 0; (i = str.indexOf("::", i)) != -1; i += 2) { + ++count; + } + return count; + } + +} diff --git a/sonar-go-checks/src/main/java/org/sonar/go/checks/IdenticalBinaryOperandCheck.java b/sonar-go-checks/src/main/java/org/sonar/go/checks/IdenticalBinaryOperandCheck.java new file mode 100644 index 00000000..e7d4d05f --- /dev/null +++ b/sonar-go-checks/src/main/java/org/sonar/go/checks/IdenticalBinaryOperandCheck.java @@ -0,0 +1,47 @@ +/* + * SonarSource Go + * Copyright (C) 2018-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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.go.checks; + +import org.sonar.check.Rule; +import org.sonarsource.slang.api.BinaryExpressionTree; +import org.sonarsource.slang.checks.api.InitContext; +import org.sonarsource.slang.checks.api.SecondaryLocation; +import org.sonarsource.slang.checks.api.SlangCheck; + +import static org.sonar.go.checks.utils.ExpressionUtils.containsPlaceHolder; +import static org.sonar.go.checks.utils.ExpressionUtils.skipParentheses; +import static org.sonarsource.slang.utils.SyntacticEquivalence.areEquivalent; + +@Rule(key = "S1764") +public class IdenticalBinaryOperandCheck implements SlangCheck { + + @Override + public void initialize(InitContext init) { + init.register(BinaryExpressionTree.class, (ctx, tree) -> { + if (tree.operator() != BinaryExpressionTree.Operator.PLUS + && tree.operator() != BinaryExpressionTree.Operator.TIMES + && !containsPlaceHolder(tree) + && areEquivalent(skipParentheses(tree.leftOperand()), skipParentheses(tree.rightOperand()))) { + ctx.reportIssue( + tree.rightOperand(), + "Correct one of the identical sub-expressions on both sides this operator", + new SecondaryLocation(tree.leftOperand())); + } + }); + } + +} diff --git a/sonar-go-checks/src/main/java/org/sonar/go/checks/IdenticalConditionsCheck.java b/sonar-go-checks/src/main/java/org/sonar/go/checks/IdenticalConditionsCheck.java new file mode 100644 index 00000000..8581ec5f --- /dev/null +++ b/sonar-go-checks/src/main/java/org/sonar/go/checks/IdenticalConditionsCheck.java @@ -0,0 +1,80 @@ +/* + * SonarSource Go + * Copyright (C) 2018-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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.go.checks; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import org.sonar.check.Rule; +import org.sonar.go.checks.utils.ExpressionUtils; +import org.sonarsource.slang.api.IfTree; +import org.sonarsource.slang.api.MatchCaseTree; +import org.sonarsource.slang.api.MatchTree; +import org.sonarsource.slang.api.TextRange; +import org.sonarsource.slang.api.Tree; +import org.sonarsource.slang.checks.api.CheckContext; +import org.sonarsource.slang.checks.api.InitContext; +import org.sonarsource.slang.checks.api.SecondaryLocation; +import org.sonarsource.slang.checks.api.SlangCheck; +import org.sonarsource.slang.utils.SyntacticEquivalence; + +import static org.sonar.go.checks.utils.ExpressionUtils.skipParentheses; + +@Rule(key = "S1862") +public class IdenticalConditionsCheck implements SlangCheck { + + @Override + public void initialize(InitContext init) { + init.register(MatchTree.class, (ctx, tree) -> checkConditions(ctx, collectConditions(tree))); + init.register(IfTree.class, (ctx, tree) -> { + if (!(ctx.parent() instanceof IfTree)) { + checkConditions(ctx, collectConditions(tree, new ArrayList<>())); + } + }); + } + + private static List collectConditions(MatchTree matchTree) { + return matchTree.cases().stream() + .map(MatchCaseTree::expression) + .filter(Objects::nonNull) + .map(ExpressionUtils::skipParentheses) + .toList(); + } + + private static List collectConditions(IfTree ifTree, List list) { + list.add(skipParentheses(ifTree.condition())); + Tree elseBranch = ifTree.elseBranch(); + if (elseBranch instanceof IfTree elseIfBranch) { + return collectConditions(elseIfBranch, list); + } + return list; + } + + private static void checkConditions(CheckContext ctx, List conditions) { + for (List group : SyntacticEquivalence.findDuplicatedGroups(conditions)) { + Tree original = group.get(0); + group.stream().skip(1) + .forEach(duplicated -> { + TextRange originalRange = original.metaData().textRange(); + ctx.reportIssue( + duplicated, + "This condition duplicates the one on line " + originalRange.start().line() + ".", + new SecondaryLocation(originalRange, "Original")); + }); + } + } +} diff --git a/sonar-go-checks/src/main/java/org/sonar/go/checks/IfConditionalAlwaysTrueOrFalseCheck.java b/sonar-go-checks/src/main/java/org/sonar/go/checks/IfConditionalAlwaysTrueOrFalseCheck.java new file mode 100644 index 00000000..47d7ebe5 --- /dev/null +++ b/sonar-go-checks/src/main/java/org/sonar/go/checks/IfConditionalAlwaysTrueOrFalseCheck.java @@ -0,0 +1,76 @@ +/* + * SonarSource Go + * Copyright (C) 2018-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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.go.checks; + +import java.util.function.Predicate; +import org.sonar.check.Rule; +import org.sonar.go.checks.utils.ExpressionUtils; +import org.sonarsource.slang.api.BinaryExpressionTree.Operator; +import org.sonarsource.slang.api.IdentifierTree; +import org.sonarsource.slang.api.IfTree; +import org.sonarsource.slang.api.LiteralTree; +import org.sonarsource.slang.api.Tree; +import org.sonarsource.slang.checks.api.InitContext; +import org.sonarsource.slang.checks.api.SlangCheck; + +import static org.sonar.go.checks.utils.ExpressionUtils.isBinaryOperation; +import static org.sonar.go.checks.utils.ExpressionUtils.isBooleanLiteral; +import static org.sonar.go.checks.utils.ExpressionUtils.isFalseValueLiteral; +import static org.sonar.go.checks.utils.ExpressionUtils.isNegation; +import static org.sonar.go.checks.utils.ExpressionUtils.isTrueValueLiteral; +import static org.sonar.go.checks.utils.ExpressionUtils.skipParentheses; +import static org.sonarsource.slang.api.BinaryExpressionTree.Operator.CONDITIONAL_AND; +import static org.sonarsource.slang.api.BinaryExpressionTree.Operator.CONDITIONAL_OR; + +@Rule(key = "S1145") +public class IfConditionalAlwaysTrueOrFalseCheck implements SlangCheck { + + public static final String MESSAGE_TEMPLATE = "Remove this useless \"%s\" statement."; + + @Override + public void initialize(InitContext init) { + init.register(IfTree.class, (ctx, ifTree) -> { + Tree condition = ifTree.condition(); + if (isAlwaysTrueOrFalse(condition)) { + String message = String.format(MESSAGE_TEMPLATE, ifTree.ifKeyword().text()); + ctx.reportIssue(condition, message); + } + }); + } + + private static boolean isAlwaysTrueOrFalse(Tree originalCondition) { + Tree condition = skipParentheses(originalCondition); + return isBooleanLiteral(condition) + || isTrueValueLiteral(condition) + || isFalseValueLiteral(condition) + || isSimpleExpressionWithLiteral(condition, CONDITIONAL_AND, ExpressionUtils::isFalseValueLiteral) + || isSimpleExpressionWithLiteral(condition, CONDITIONAL_OR, ExpressionUtils::isTrueValueLiteral); + } + + private static boolean isSimpleExpressionWithLiteral(Tree condition, Operator operator, Predicate hasLiteralValue) { + boolean simpleExpression = isBinaryOperation(condition, operator) + && condition.descendants() + .map(ExpressionUtils::skipParentheses) + .allMatch(tree -> tree instanceof IdentifierTree + || tree instanceof LiteralTree + || isNegation(tree) + || isBinaryOperation(tree, operator)); + + return simpleExpression && condition.descendants().anyMatch(hasLiteralValue); + } + +} diff --git a/sonar-go-checks/src/main/java/org/sonar/go/checks/MatchCaseTooBigCheck.java b/sonar-go-checks/src/main/java/org/sonar/go/checks/MatchCaseTooBigCheck.java new file mode 100644 index 00000000..c7efdc4e --- /dev/null +++ b/sonar-go-checks/src/main/java/org/sonar/go/checks/MatchCaseTooBigCheck.java @@ -0,0 +1,49 @@ +/* + * SonarSource Go + * Copyright (C) 2018-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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.go.checks; + +import java.text.MessageFormat; +import org.sonar.check.Rule; +import org.sonar.check.RuleProperty; +import org.sonarsource.slang.api.MatchCaseTree; +import org.sonarsource.slang.checks.api.InitContext; +import org.sonarsource.slang.checks.api.SlangCheck; + +@Rule(key = "S1151") +public class MatchCaseTooBigCheck implements SlangCheck { + + private static final int DEFAULT_MAX = 6; + private static final String DEFAULT_MAX_VALUE = "" + DEFAULT_MAX; + + private static final String MESSAGE = "Reduce this case clause number of lines from {0} to at most {1}, for example by extracting code into methods."; + + @RuleProperty( + key = "max", + description = "Maximum number of lines", + defaultValue = DEFAULT_MAX_VALUE) + public int max = DEFAULT_MAX; + + @Override + public void initialize(InitContext init) { + init.register(MatchCaseTree.class, (ctx, matchCaseTree) -> { + int linesOfCode = matchCaseTree.metaData().linesOfCode().size(); + if (linesOfCode > max) { + ctx.reportIssue(matchCaseTree.rangeToHighlight(), MessageFormat.format(MESSAGE, linesOfCode, max)); + } + }); + } +} diff --git a/sonar-go-checks/src/main/java/org/sonar/go/checks/MatchWithoutElseCheck.java b/sonar-go-checks/src/main/java/org/sonar/go/checks/MatchWithoutElseCheck.java new file mode 100644 index 00000000..471de522 --- /dev/null +++ b/sonar-go-checks/src/main/java/org/sonar/go/checks/MatchWithoutElseCheck.java @@ -0,0 +1,39 @@ +/* + * SonarSource Go + * Copyright (C) 2018-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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.go.checks; + +import org.sonar.check.Rule; +import org.sonarsource.slang.api.MatchTree; +import org.sonarsource.slang.api.Token; +import org.sonarsource.slang.checks.api.InitContext; +import org.sonarsource.slang.checks.api.SlangCheck; + +@Rule(key = "S131") +public class MatchWithoutElseCheck implements SlangCheck { + + @Override + public void initialize(InitContext init) { + init.register(MatchTree.class, (ctx, tree) -> { + if (tree.cases().stream().noneMatch(matchCase -> matchCase.expression() == null)) { + Token keyword = tree.keyword(); + String message = String.format("Add a default clause to this \"%s\" statement.", keyword.text()); + ctx.reportIssue(keyword, message); + } + }); + } + +} diff --git a/sonar-go-plugin/src/main/java/org/sonar/go/checks/NativeKinds.java b/sonar-go-checks/src/main/java/org/sonar/go/checks/NativeKinds.java similarity index 100% rename from sonar-go-plugin/src/main/java/org/sonar/go/checks/NativeKinds.java rename to sonar-go-checks/src/main/java/org/sonar/go/checks/NativeKinds.java diff --git a/sonar-go-checks/src/main/java/org/sonar/go/checks/NestedMatchCheck.java b/sonar-go-checks/src/main/java/org/sonar/go/checks/NestedMatchCheck.java new file mode 100644 index 00000000..3f0928a4 --- /dev/null +++ b/sonar-go-checks/src/main/java/org/sonar/go/checks/NestedMatchCheck.java @@ -0,0 +1,36 @@ +/* + * SonarSource Go + * Copyright (C) 2018-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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.go.checks; + +import java.text.MessageFormat; +import org.sonar.check.Rule; +import org.sonarsource.slang.api.MatchTree; +import org.sonarsource.slang.checks.api.InitContext; +import org.sonarsource.slang.checks.api.SlangCheck; + +@Rule(key = "S1821") +public class NestedMatchCheck implements SlangCheck { + private static final String MESSAGE = "Refactor the code to eliminate this nested \"{0}\"."; + + @Override + public void initialize(InitContext init) { + init.register(MatchTree.class, (ctx, matchTree) -> ctx.ancestors().stream() + .filter(MatchTree.class::isInstance) + .findFirst() + .ifPresent(parentMatch -> ctx.reportIssue(matchTree.keyword(), MessageFormat.format(MESSAGE, matchTree.keyword().text())))); + } +} diff --git a/sonar-go-checks/src/main/java/org/sonar/go/checks/OctalValuesCheck.java b/sonar-go-checks/src/main/java/org/sonar/go/checks/OctalValuesCheck.java new file mode 100644 index 00000000..a6236940 --- /dev/null +++ b/sonar-go-checks/src/main/java/org/sonar/go/checks/OctalValuesCheck.java @@ -0,0 +1,49 @@ +/* + * SonarSource Go + * Copyright (C) 2018-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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.go.checks; + +import java.math.BigInteger; +import org.sonar.check.Rule; +import org.sonarsource.slang.api.IntegerLiteralTree; +import org.sonarsource.slang.checks.api.InitContext; +import org.sonarsource.slang.checks.api.SlangCheck; + +import static org.sonarsource.slang.api.IntegerLiteralTree.Base.OCTAL; + +@Rule(key = "S1314") +public class OctalValuesCheck implements SlangCheck { + + private static final String MESSAGE = "Use decimal values instead of octal ones."; + private static final BigInteger EIGHT = BigInteger.valueOf(OCTAL.getRadix()); + private static final int FILE_PERMISSION_MASK_LENGTH = 3; + + @Override + public void initialize(InitContext init) { + init.register(IntegerLiteralTree.class, (ctx, literal) -> { + if (literal.getBase() == OCTAL && !isException(literal)) { + ctx.reportIssue(literal, MESSAGE); + } + }); + } + + private static boolean isException(IntegerLiteralTree literalTree) { + // octal literal < 8 are authorized, as well as octal literals with 3 digits, as they are often used for file permissions + BigInteger value = literalTree.getIntegerValue(); + return value.compareTo(EIGHT) < 0 || literalTree.getNumericPart().length() == FILE_PERMISSION_MASK_LENGTH; + } + +} diff --git a/sonar-go-checks/src/main/java/org/sonar/go/checks/OneStatementPerLineGoCheck.java b/sonar-go-checks/src/main/java/org/sonar/go/checks/OneStatementPerLineGoCheck.java new file mode 100644 index 00000000..30cdb3ec --- /dev/null +++ b/sonar-go-checks/src/main/java/org/sonar/go/checks/OneStatementPerLineGoCheck.java @@ -0,0 +1,73 @@ +/* + * SonarSource Go + * Copyright (C) 2018-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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.go.checks; + +import java.util.List; +import java.util.stream.Collectors; +import org.sonar.check.Rule; +import org.sonarsource.slang.api.BlockTree; +import org.sonarsource.slang.api.NativeTree; +import org.sonarsource.slang.api.TopLevelTree; +import org.sonarsource.slang.api.Tree; +import org.sonarsource.slang.checks.api.CheckContext; +import org.sonarsource.slang.checks.api.InitContext; +import org.sonarsource.slang.checks.api.SecondaryLocation; +import org.sonarsource.slang.checks.api.SlangCheck; + +import static org.sonar.go.checks.NativeKinds.SEMICOLON; + +@Rule(key = "S122") +public class OneStatementPerLineGoCheck implements SlangCheck { + private static final String MESSAGE = "Reformat the code to have only one statement per line."; + + @Override + public void initialize(InitContext init) { + init.register(TopLevelTree.class, (ctx, topLevelTree) -> checkStatements(ctx, topLevelTree.children())); + init.register(BlockTree.class, (ctx, blockTree) -> checkStatements(ctx, blockTree.statementOrExpressions())); + } + + private static void checkStatements(CheckContext ctx, List statementsOrExpressions) { + statementsOrExpressions.stream() + .filter(tree -> !shouldIgnore(tree)) + .collect(Collectors.groupingBy(OneStatementPerLineGoCheck::getLine)) + .forEach((line, statements) -> { + if (statements.size() > 1) { + reportIssue(ctx, statements); + } + }); + } + + private static void reportIssue(CheckContext ctx, List statements) { + List secondaryLocations = statements.stream() + .skip(2) + .map(statement -> new SecondaryLocation(statement, null)) + .toList(); + ctx.reportIssue( + statements.get(1), + MESSAGE, + secondaryLocations); + } + + private static int getLine(Tree statementOrExpression) { + return statementOrExpression.metaData().textRange().start().line(); + } + + private static boolean shouldIgnore(Tree tree) { + return tree instanceof NativeTree nativeTree && + nativeTree.nativeKind().toString().equals(SEMICOLON); + } +} diff --git a/sonar-go-plugin/src/main/java/org/sonar/go/checks/OneStatementPerLineGoCheck.java b/sonar-go-checks/src/main/java/org/sonar/go/checks/ParsingErrorCheck.java similarity index 62% rename from sonar-go-plugin/src/main/java/org/sonar/go/checks/OneStatementPerLineGoCheck.java rename to sonar-go-checks/src/main/java/org/sonar/go/checks/ParsingErrorCheck.java index 2ca52ba4..73a3a854 100644 --- a/sonar-go-plugin/src/main/java/org/sonar/go/checks/OneStatementPerLineGoCheck.java +++ b/sonar-go-checks/src/main/java/org/sonar/go/checks/ParsingErrorCheck.java @@ -17,17 +17,13 @@ package org.sonar.go.checks; import org.sonar.check.Rule; -import org.sonarsource.slang.api.NativeTree; -import org.sonarsource.slang.api.Tree; -import org.sonarsource.slang.checks.OneStatementPerLineCheck; +import org.sonarsource.slang.checks.api.InitContext; +import org.sonarsource.slang.checks.api.SlangCheck; -import static org.sonar.go.checks.NativeKinds.SEMICOLON; - -@Rule(key = "S122") -public class OneStatementPerLineGoCheck extends OneStatementPerLineCheck { +@Rule(key = "S2260") +public class ParsingErrorCheck implements SlangCheck { @Override - protected boolean shouldIgnore(Tree tree) { - return tree instanceof NativeTree && - ((NativeTree) tree).nativeKind().toString().equals(SEMICOLON); + public void initialize(InitContext init) { + // errors are reported in InputFileContext#reportParseError } } diff --git a/sonar-go-checks/src/main/java/org/sonar/go/checks/RedundantParenthesesCheck.java b/sonar-go-checks/src/main/java/org/sonar/go/checks/RedundantParenthesesCheck.java new file mode 100644 index 00000000..0e4aea57 --- /dev/null +++ b/sonar-go-checks/src/main/java/org/sonar/go/checks/RedundantParenthesesCheck.java @@ -0,0 +1,38 @@ +/* + * SonarSource Go + * Copyright (C) 2018-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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.go.checks; + +import org.sonar.check.Rule; +import org.sonarsource.slang.api.ParenthesizedExpressionTree; +import org.sonarsource.slang.checks.api.InitContext; +import org.sonarsource.slang.checks.api.SecondaryLocation; +import org.sonarsource.slang.checks.api.SlangCheck; + +@Rule(key = "S1110") +public class RedundantParenthesesCheck implements SlangCheck { + + @Override + public void initialize(InitContext init) { + init.register(ParenthesizedExpressionTree.class, (ctx, tree) -> { + if (ctx.parent() instanceof ParenthesizedExpressionTree) { + SecondaryLocation secondaryLocation = new SecondaryLocation(tree.rightParenthesis().textRange(), null); + ctx.reportIssue(tree.leftParenthesis(), "Remove these useless parentheses.", secondaryLocation); + } + }); + } + +} diff --git a/sonar-go-checks/src/main/java/org/sonar/go/checks/SelfAssignmentCheck.java b/sonar-go-checks/src/main/java/org/sonar/go/checks/SelfAssignmentCheck.java new file mode 100644 index 00000000..4f737e58 --- /dev/null +++ b/sonar-go-checks/src/main/java/org/sonar/go/checks/SelfAssignmentCheck.java @@ -0,0 +1,38 @@ +/* + * SonarSource Go + * Copyright (C) 2018-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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.go.checks; + +import org.sonar.check.Rule; +import org.sonarsource.slang.api.AssignmentExpressionTree; +import org.sonarsource.slang.checks.api.InitContext; +import org.sonarsource.slang.checks.api.SlangCheck; + +import static org.sonarsource.slang.utils.SyntacticEquivalence.areEquivalent; + +@Rule(key = "S1656") +public class SelfAssignmentCheck implements SlangCheck { + + @Override + public void initialize(InitContext init) { + init.register(AssignmentExpressionTree.class, (ctx, tree) -> { + if (tree.operator() == AssignmentExpressionTree.Operator.EQUAL && areEquivalent(tree.leftHandSide(), tree.statementOrExpression())) { + ctx.reportIssue(tree, "Remove or correct this useless self-assignment."); + } + }); + } + +} diff --git a/sonar-go-checks/src/main/java/org/sonar/go/checks/StringLiteralDuplicatedCheck.java b/sonar-go-checks/src/main/java/org/sonar/go/checks/StringLiteralDuplicatedCheck.java new file mode 100644 index 00000000..1016e841 --- /dev/null +++ b/sonar-go-checks/src/main/java/org/sonar/go/checks/StringLiteralDuplicatedCheck.java @@ -0,0 +1,76 @@ +/* + * SonarSource Go + * Copyright (C) 2018-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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.go.checks; + +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; +import org.sonar.check.Rule; +import org.sonar.check.RuleProperty; +import org.sonarsource.slang.api.StringLiteralTree; +import org.sonarsource.slang.api.TopLevelTree; +import org.sonarsource.slang.checks.api.CheckContext; +import org.sonarsource.slang.checks.api.InitContext; +import org.sonarsource.slang.checks.api.SecondaryLocation; +import org.sonarsource.slang.checks.api.SlangCheck; + +@Rule(key = "S1192") +public class StringLiteralDuplicatedCheck implements SlangCheck { + + private static final int DEFAULT_THRESHOLD = 3; + private static final int MINIMAL_LITERAL_LENGTH = 5; + private static final Pattern NO_SEPARATOR_REGEXP = Pattern.compile("\\w++"); + + @RuleProperty( + key = "threshold", + description = "Number of times a literal must be duplicated to trigger an issue", + defaultValue = "" + DEFAULT_THRESHOLD) + public int threshold = DEFAULT_THRESHOLD; + + @Override + public void initialize(InitContext init) { + init.register(TopLevelTree.class, (ctx, tree) -> { + Map> occurrences = new HashMap<>(); + tree.descendants() + .filter(StringLiteralTree.class::isInstance) + .map(StringLiteralTree.class::cast) + .filter(literal -> literal.content().length() > MINIMAL_LITERAL_LENGTH && !NO_SEPARATOR_REGEXP.matcher(literal.content()).matches()) + .forEach(literal -> occurrences.computeIfAbsent(literal.content(), key -> new LinkedList<>()).add(literal)); + check(ctx, occurrences, threshold); + }); + } + + private static void check(CheckContext ctx, Map> occurrencesMap, int threshold) { + for (Map.Entry> entry : occurrencesMap.entrySet()) { + List occurrences = entry.getValue(); + int size = occurrences.size(); + if (size >= threshold) { + StringLiteralTree first = occurrences.get(0); + String message = String.format("Define a constant instead of duplicating this literal \"%s\" %s times.", first.content(), size); + List secondaryLocations = occurrences.stream() + .skip(1) + .map(stringLiteral -> new SecondaryLocation(stringLiteral.metaData().textRange(), "Duplication")) + .toList(); + double gap = size - 1.0; + ctx.reportIssue(first, message, secondaryLocations, gap); + } + } + } + +} diff --git a/sonar-go-checks/src/main/java/org/sonar/go/checks/TodoCommentCheck.java b/sonar-go-checks/src/main/java/org/sonar/go/checks/TodoCommentCheck.java new file mode 100644 index 00000000..9b4293f4 --- /dev/null +++ b/sonar-go-checks/src/main/java/org/sonar/go/checks/TodoCommentCheck.java @@ -0,0 +1,51 @@ +/* + * SonarSource Go + * Copyright (C) 2018-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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.go.checks; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.sonar.check.Rule; +import org.sonarsource.analyzer.commons.TokenLocation; +import org.sonarsource.slang.api.TextPointer; +import org.sonarsource.slang.api.TextRange; +import org.sonarsource.slang.api.TopLevelTree; +import org.sonarsource.slang.checks.api.InitContext; +import org.sonarsource.slang.checks.api.SlangCheck; +import org.sonarsource.slang.impl.TextPointerImpl; +import org.sonarsource.slang.impl.TextRangeImpl; + +@Rule(key = "S1135") +public class TodoCommentCheck implements SlangCheck { + + private final Pattern todoPattern = Pattern.compile("(?i)(^|[[^\\p{L}]&&\\D])(todo)($|[[^\\p{L}]&&\\D])"); + + @Override + public void initialize(InitContext init) { + init.register(TopLevelTree.class, (ctx, tree) -> tree.allComments().forEach(comment -> { + Matcher matcher = todoPattern.matcher(comment.text()); + if (matcher.find()) { + TextPointer start = comment.textRange().start(); + TokenLocation location = new TokenLocation(start.line(), start.lineOffset(), comment.text().substring(0, matcher.start(2))); + TextRange todoRange = new TextRangeImpl( + new TextPointerImpl(location.endLine(), location.endLineOffset()), + new TextPointerImpl(location.endLine(), location.endLineOffset() + 4)); + ctx.reportIssue(todoRange, "Complete the task associated to this TODO comment."); + } + })); + } + +} diff --git a/sonar-go-checks/src/main/java/org/sonar/go/checks/TooComplexExpressionCheck.java b/sonar-go-checks/src/main/java/org/sonar/go/checks/TooComplexExpressionCheck.java new file mode 100644 index 00000000..2da74424 --- /dev/null +++ b/sonar-go-checks/src/main/java/org/sonar/go/checks/TooComplexExpressionCheck.java @@ -0,0 +1,88 @@ +/* + * SonarSource Go + * Copyright (C) 2018-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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.go.checks; + +import java.util.Collections; +import java.util.Iterator; +import org.sonar.check.Rule; +import org.sonar.check.RuleProperty; +import org.sonarsource.slang.api.BinaryExpressionTree; +import org.sonarsource.slang.api.ParenthesizedExpressionTree; +import org.sonarsource.slang.api.Tree; +import org.sonarsource.slang.api.UnaryExpressionTree; +import org.sonarsource.slang.checks.api.CheckContext; +import org.sonarsource.slang.checks.api.InitContext; +import org.sonarsource.slang.checks.api.SlangCheck; + +import static org.sonar.go.checks.utils.ExpressionUtils.isLogicalBinaryExpression; +import static org.sonar.go.checks.utils.ExpressionUtils.skipParentheses; + +@Rule(key = "S1067") +public class TooComplexExpressionCheck implements SlangCheck { + + private static final int DEFAULT_MAX_COMPLEXITY = 3; + + @RuleProperty(key = "max", + description = "Maximum number of allowed conditional operators in an expression", + defaultValue = "" + DEFAULT_MAX_COMPLEXITY) + public int max = DEFAULT_MAX_COMPLEXITY; + + @Override + public void initialize(InitContext init) { + init.register(BinaryExpressionTree.class, (ctx, tree) -> { + if (isParentExpression(ctx)) { + int complexity = computeExpressionComplexity(tree); + if (complexity > max) { + String message = String.format( + "Reduce the number of conditional operators (%s) used in the expression (maximum allowed %s).", + complexity, + max); + double gap = (double) complexity - max; + ctx.reportIssue(tree, message, Collections.emptyList(), gap); + } + } + }); + } + + private static boolean isParentExpression(CheckContext ctx) { + Iterator iterator = ctx.ancestors().iterator(); + while (iterator.hasNext()) { + Tree parentExpression = iterator.next(); + if (parentExpression instanceof BinaryExpressionTree) { + return false; + } else if (!(parentExpression instanceof UnaryExpressionTree) || !(parentExpression instanceof ParenthesizedExpressionTree)) { + return true; + } + } + return true; + } + + private static int computeExpressionComplexity(Tree originalTree) { + Tree tree = skipParentheses(originalTree); + if (tree instanceof BinaryExpressionTree binary) { + int complexity = isLogicalBinaryExpression(tree) ? 1 : 0; + return complexity + + computeExpressionComplexity(binary.leftOperand()) + + computeExpressionComplexity(binary.rightOperand()); + } else if (tree instanceof UnaryExpressionTree unaryExpressionTree) { + return computeExpressionComplexity(unaryExpressionTree.operand()); + } else { + return 0; + } + } + +} diff --git a/sonar-go-checks/src/main/java/org/sonar/go/checks/TooDeeplyNestedStatementsCheck.java b/sonar-go-checks/src/main/java/org/sonar/go/checks/TooDeeplyNestedStatementsCheck.java new file mode 100644 index 00000000..2c9cfbfc --- /dev/null +++ b/sonar-go-checks/src/main/java/org/sonar/go/checks/TooDeeplyNestedStatementsCheck.java @@ -0,0 +1,122 @@ +/* + * SonarSource Go + * Copyright (C) 2018-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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.go.checks; + +import java.util.ArrayList; +import java.util.Deque; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import javax.annotation.Nullable; +import org.sonar.check.Rule; +import org.sonar.check.RuleProperty; +import org.sonar.go.checks.utils.ExpressionUtils; +import org.sonarsource.slang.api.ExceptionHandlingTree; +import org.sonarsource.slang.api.IfTree; +import org.sonarsource.slang.api.LoopTree; +import org.sonarsource.slang.api.MatchTree; +import org.sonarsource.slang.api.Token; +import org.sonarsource.slang.api.Tree; +import org.sonarsource.slang.checks.api.CheckContext; +import org.sonarsource.slang.checks.api.InitContext; +import org.sonarsource.slang.checks.api.SecondaryLocation; +import org.sonarsource.slang.checks.api.SlangCheck; + +@Rule(key = "S134") +public class TooDeeplyNestedStatementsCheck implements SlangCheck { + private static final int DEFAULT_MAX_DEPTH = 4; + private static final String DEFAULT_MAX_DEPTH_VALUE = "" + DEFAULT_MAX_DEPTH; + + @RuleProperty( + key = "max", + description = "Maximum allowed control flow statement nesting depth", + defaultValue = DEFAULT_MAX_DEPTH_VALUE) + public int max = DEFAULT_MAX_DEPTH; + + @Override + public void initialize(InitContext init) { + init.register(IfTree.class, this::checkNestedDepth); + init.register(LoopTree.class, this::checkNestedDepth); + init.register(MatchTree.class, this::checkNestedDepth); + init.register(ExceptionHandlingTree.class, this::checkNestedDepth); + } + + private void checkNestedDepth(CheckContext ctx, Tree tree) { + if (isElseIfStatement(ctx.parent(), tree)) { + // Ignore 'else-if' statements since the issue would already be raised on the first 'if' statement + return; + } + if (ExpressionUtils.isTernaryOperator(ctx.ancestors(), tree)) { + return; + } + + Iterator iterator = ctx.ancestors().iterator(); + Deque nestedParentNodes = new LinkedList<>(); + Tree last = tree; + + while (iterator.hasNext()) { + Tree parent = iterator.next(); + if (isElseIfStatement(parent, last) && !nestedParentNodes.isEmpty()) { + // Only the 'if' parent of the chained 'else-if' statements should be highlighted + nestedParentNodes.removeLast(); + } + if (parent instanceof LoopTree || parent instanceof ExceptionHandlingTree || parent instanceof IfTree || parent instanceof MatchTree) { + nestedParentNodes.addLast(getNodeToHighlight(parent)); + } + if (nestedParentNodes.size() > max) { + return; + } + last = parent; + } + + if (nestedParentNodes.size() == max) { + reportIssue(ctx, tree, nestedParentNodes); + } + } + + private static boolean isElseIfStatement(@Nullable Tree parent, @Nullable Tree tree) { + return tree instanceof IfTree && parent instanceof IfTree && tree.equals(((IfTree) parent).elseBranch()); + } + + private void reportIssue(CheckContext ctx, Tree statement, Deque nestedStatements) { + String message = String.format("Refactor this code to not nest more than %s control flow statements.", max); + List secondaryLocations = new ArrayList<>(nestedStatements.size()); + int nestedDepth = 0; + + while (!nestedStatements.isEmpty()) { + nestedDepth++; + String secondaryLocationMessage = String.format("Nesting depth %s", nestedDepth); + secondaryLocations.add(new SecondaryLocation(nestedStatements.removeLast().textRange(), secondaryLocationMessage)); + } + + Token nodeToHighlight = getNodeToHighlight(statement); + ctx.reportIssue(nodeToHighlight, message, secondaryLocations); + } + + private static Token getNodeToHighlight(Tree tree) { + if (tree instanceof IfTree ifTree) { + return ifTree.ifKeyword(); + } else if (tree instanceof MatchTree matchTree) { + return matchTree.keyword(); + } else if (tree instanceof ExceptionHandlingTree exceptionHandlingTree) { + return exceptionHandlingTree.tryKeyword(); + } else { + return ((LoopTree) tree).keyword(); + } + } + +} diff --git a/sonar-go-checks/src/main/java/org/sonar/go/checks/TooLongFunctionCheck.java b/sonar-go-checks/src/main/java/org/sonar/go/checks/TooLongFunctionCheck.java new file mode 100644 index 00000000..07a1e84f --- /dev/null +++ b/sonar-go-checks/src/main/java/org/sonar/go/checks/TooLongFunctionCheck.java @@ -0,0 +1,56 @@ +/* + * SonarSource Go + * Copyright (C) 2018-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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.go.checks; + +import org.sonar.check.Rule; +import org.sonar.check.RuleProperty; +import org.sonarsource.slang.api.BlockTree; +import org.sonarsource.slang.api.FunctionDeclarationTree; +import org.sonarsource.slang.checks.api.InitContext; +import org.sonarsource.slang.checks.api.SlangCheck; + +@Rule(key = "S138") +public class TooLongFunctionCheck implements SlangCheck { + + private static final int DEFAULT_MAX = 120; + private static final String DEFAULT_MAX_VALUE = "" + DEFAULT_MAX; + + @RuleProperty( + key = "max", + description = "Maximum authorized lines of code in a function", + defaultValue = DEFAULT_MAX_VALUE) + public int max = DEFAULT_MAX; + + @Override + public void initialize(InitContext init) { + init.register(FunctionDeclarationTree.class, (ctx, tree) -> { + BlockTree body = tree.body(); + if (body == null) { + return; + } + int numberOfLinesOfCode = body.metaData().linesOfCode().size(); + if (numberOfLinesOfCode > max) { + String message = String.format( + "This function has %s lines of code, which is greater than the %s authorized. Split it into smaller functions.", + numberOfLinesOfCode, + max); + ctx.reportIssue(tree.rangeToHighlight(), message); + } + }); + } + +} diff --git a/sonar-go-checks/src/main/java/org/sonar/go/checks/TooLongLineCheck.java b/sonar-go-checks/src/main/java/org/sonar/go/checks/TooLongLineCheck.java new file mode 100644 index 00000000..72b6480b --- /dev/null +++ b/sonar-go-checks/src/main/java/org/sonar/go/checks/TooLongLineCheck.java @@ -0,0 +1,62 @@ +/* + * SonarSource Go + * Copyright (C) 2018-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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.go.checks; + +import java.text.MessageFormat; +import java.util.stream.IntStream; +import org.sonar.check.Rule; +import org.sonar.check.RuleProperty; +import org.sonarsource.slang.api.TextRange; +import org.sonarsource.slang.api.TopLevelTree; +import org.sonarsource.slang.checks.api.InitContext; +import org.sonarsource.slang.checks.api.SlangCheck; +import org.sonarsource.slang.impl.TextPointerImpl; +import org.sonarsource.slang.impl.TextRangeImpl; + +@Rule(key = "S103") +public class TooLongLineCheck implements SlangCheck { + private static final int DEFAULT_MAXIMUM_LINE_LENGTH = 120; + private static final String DEFAULT_MAXIMUM_LINE_LENGTH_VALUE = "" + DEFAULT_MAXIMUM_LINE_LENGTH; + + @RuleProperty( + key = "maximumLineLength", + description = "The maximum authorized line length.", + defaultValue = DEFAULT_MAXIMUM_LINE_LENGTH_VALUE) + int maximumLineLength = DEFAULT_MAXIMUM_LINE_LENGTH; + + private static final String MESSAGE = "Split this {0} characters long line (which is greater than {1} authorized)."; + + @Override + public void initialize(InitContext init) { + init.register(TopLevelTree.class, ((ctx, topLevelTree) -> { + String[] lines = ctx.fileContent().split("\r\n|\n|\r", -1); + IntStream.range(0, lines.length) + .filter(lineNumber -> lines[lineNumber].length() > maximumLineLength) + .forEach(lineNumber -> { + int lineLength = lines[lineNumber].length(); + TextRange longLine = getLineRange(lineNumber + 1, lineLength); + ctx.reportIssue(longLine, MessageFormat.format(MESSAGE, lineLength, maximumLineLength)); + }); + })); + } + + private static TextRange getLineRange(int lineNumber, int lineLength) { + return new TextRangeImpl( + new TextPointerImpl(lineNumber, 0), + new TextPointerImpl(lineNumber, lineLength)); + } +} diff --git a/sonar-go-checks/src/main/java/org/sonar/go/checks/TooManyCasesCheck.java b/sonar-go-checks/src/main/java/org/sonar/go/checks/TooManyCasesCheck.java new file mode 100644 index 00000000..c941cb1e --- /dev/null +++ b/sonar-go-checks/src/main/java/org/sonar/go/checks/TooManyCasesCheck.java @@ -0,0 +1,58 @@ +/* + * SonarSource Go + * Copyright (C) 2018-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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.go.checks; + +import java.util.List; +import org.sonar.check.Rule; +import org.sonar.check.RuleProperty; +import org.sonarsource.slang.api.MatchTree; +import org.sonarsource.slang.api.Token; +import org.sonarsource.slang.checks.api.InitContext; +import org.sonarsource.slang.checks.api.SecondaryLocation; +import org.sonarsource.slang.checks.api.SlangCheck; + +@Rule(key = "S1479") +public class TooManyCasesCheck implements SlangCheck { + + private static final int DEFAULT_MAX = 30; + + @RuleProperty( + key = "maximum", + description = "Maximum number of branches", + defaultValue = "" + DEFAULT_MAX) + public int maximum = DEFAULT_MAX; + + @Override + public void initialize(InitContext init) { + init.register(MatchTree.class, (ctx, tree) -> { + int numberOfCases = tree.cases().size(); + if (numberOfCases > maximum) { + Token matchKeyword = tree.keyword(); + String message = String.format( + "Reduce the number of %s branches from %s to at most %s.", + matchKeyword.text(), + numberOfCases, + maximum); + List secondaryLocations = tree.cases().stream() + .map(matchCase -> new SecondaryLocation(matchCase.rangeToHighlight(), null)) + .toList(); + ctx.reportIssue(matchKeyword, message, secondaryLocations); + } + }); + } + +} diff --git a/sonar-go-checks/src/main/java/org/sonar/go/checks/TooManyLinesOfCodeFileCheck.java b/sonar-go-checks/src/main/java/org/sonar/go/checks/TooManyLinesOfCodeFileCheck.java new file mode 100644 index 00000000..17a08573 --- /dev/null +++ b/sonar-go-checks/src/main/java/org/sonar/go/checks/TooManyLinesOfCodeFileCheck.java @@ -0,0 +1,50 @@ +/* + * SonarSource Go + * Copyright (C) 2018-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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.go.checks; + +import org.sonar.check.Rule; +import org.sonar.check.RuleProperty; +import org.sonarsource.slang.api.TopLevelTree; +import org.sonarsource.slang.checks.api.InitContext; +import org.sonarsource.slang.checks.api.SlangCheck; + +@Rule(key = "S104") +public class TooManyLinesOfCodeFileCheck implements SlangCheck { + + private static final int DEFAULT_MAX = 750; + private static final String DEFAULT_MAX_VALUE = "" + DEFAULT_MAX; + + @RuleProperty( + key = "Max", + description = "Maximum authorized lines of code in a file.", + defaultValue = DEFAULT_MAX_VALUE) + public int max = DEFAULT_MAX; + + @Override + public void initialize(InitContext init) { + init.register(TopLevelTree.class, (ctx, tree) -> { + int numberOfLinesOfCode = tree.metaData().linesOfCode().size(); + if (numberOfLinesOfCode > max) { + String message = String.format( + "File \"%s\" has %s lines, which is greater than %s authorized. Split it into smaller files.", + ctx.filename(), numberOfLinesOfCode, max); + ctx.reportFileIssue(message); + } + }); + } + +} diff --git a/sonar-go-checks/src/main/java/org/sonar/go/checks/TooManyParametersCheck.java b/sonar-go-checks/src/main/java/org/sonar/go/checks/TooManyParametersCheck.java new file mode 100644 index 00000000..7cf441ea --- /dev/null +++ b/sonar-go-checks/src/main/java/org/sonar/go/checks/TooManyParametersCheck.java @@ -0,0 +1,76 @@ +/* + * SonarSource Go + * Copyright (C) 2018-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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.go.checks; + +import java.util.List; +import org.sonar.check.Rule; +import org.sonar.check.RuleProperty; +import org.sonarsource.slang.api.FunctionDeclarationTree; +import org.sonarsource.slang.api.ModifierTree; +import org.sonarsource.slang.checks.api.InitContext; +import org.sonarsource.slang.checks.api.SecondaryLocation; +import org.sonarsource.slang.checks.api.SlangCheck; + +@Rule(key = "S107") +public class TooManyParametersCheck implements SlangCheck { + + private static final int DEFAULT_MAX = 7; + + @RuleProperty( + key = "Max", + description = "Maximum authorized number of parameters", + defaultValue = "" + DEFAULT_MAX) + public int max = DEFAULT_MAX; + + @Override + public void initialize(InitContext init) { + init.register(FunctionDeclarationTree.class, (ctx, tree) -> { + if (isCandidateMethod(tree)) { + String message = String.format( + "This function has %s parameters, which is greater than the %s authorized.", + tree.formalParameters().size(), + max); + List secondaryLocations = tree.formalParameters().stream() + .skip(max) + .map(SecondaryLocation::new) + .toList(); + + if (tree.name() == null) { + ctx.reportIssue(tree, message, secondaryLocations); + } else { + ctx.reportIssue(tree.name(), message, secondaryLocations); + } + } + }); + } + + protected boolean isCandidateMethod(FunctionDeclarationTree functionDeclarationTree) { + return !functionDeclarationTree.isConstructor() + && !isOverrideMethod(functionDeclarationTree) + && functionDeclarationTree.formalParameters().size() > max; + } + + private static boolean isOverrideMethod(FunctionDeclarationTree tree) { + return tree.modifiers().stream().anyMatch(mod -> { + if (!(mod instanceof ModifierTree)) { + return false; + } + return ((ModifierTree) mod).kind() == ModifierTree.Kind.OVERRIDE; + }); + } + +} diff --git a/sonar-go-checks/src/main/java/org/sonar/go/checks/VariableAndParameterNameCheck.java b/sonar-go-checks/src/main/java/org/sonar/go/checks/VariableAndParameterNameCheck.java new file mode 100644 index 00000000..d6242f59 --- /dev/null +++ b/sonar-go-checks/src/main/java/org/sonar/go/checks/VariableAndParameterNameCheck.java @@ -0,0 +1,63 @@ +/* + * SonarSource Go + * Copyright (C) 2018-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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.go.checks; + +import java.util.regex.Pattern; +import javax.annotation.Nullable; +import org.sonar.check.Rule; +import org.sonar.check.RuleProperty; +import org.sonarsource.slang.api.FunctionDeclarationTree; +import org.sonarsource.slang.api.IdentifierTree; +import org.sonarsource.slang.api.ParameterTree; +import org.sonarsource.slang.api.VariableDeclarationTree; +import org.sonarsource.slang.checks.api.CheckContext; +import org.sonarsource.slang.checks.api.InitContext; +import org.sonarsource.slang.checks.api.SlangCheck; + +@Rule(key = "S117") +public class VariableAndParameterNameCheck implements SlangCheck { + + @RuleProperty( + key = "format", + description = "Regular expression used to check the names against.", + defaultValue = GoChecksConstants.GO_NAMING_DEFAULT) + public String format = GoChecksConstants.GO_NAMING_DEFAULT; + + @Override + public void initialize(InitContext init) { + Pattern pattern = Pattern.compile(format); + + init.register(VariableDeclarationTree.class, (ctx, tree) -> { + if (ctx.ancestors().stream().anyMatch(FunctionDeclarationTree.class::isInstance)) { + check(pattern, ctx, tree.identifier(), "local variable"); + } + }); + + init.register(FunctionDeclarationTree.class, (ctx, tree) -> tree.formalParameters().stream() + .filter(ParameterTree.class::isInstance) + .map(ParameterTree.class::cast) + .forEach(param -> check(pattern, ctx, param.identifier(), "parameter"))); + } + + private void check(Pattern pattern, CheckContext ctx, @Nullable IdentifierTree identifier, String variableKind) { + if (identifier != null && !pattern.matcher(identifier.name()).matches()) { + String message = String.format("Rename this %s to match the regular expression \"%s\".", variableKind, this.format); + ctx.reportIssue(identifier, message); + } + } + +} diff --git a/sonar-go-checks/src/main/java/org/sonar/go/checks/WrongAssignmentOperatorCheck.java b/sonar-go-checks/src/main/java/org/sonar/go/checks/WrongAssignmentOperatorCheck.java new file mode 100644 index 00000000..7ec46bea --- /dev/null +++ b/sonar-go-checks/src/main/java/org/sonar/go/checks/WrongAssignmentOperatorCheck.java @@ -0,0 +1,84 @@ +/* + * SonarSource Go + * Copyright (C) 2018-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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.go.checks; + +import java.util.List; +import org.sonar.check.Rule; +import org.sonarsource.slang.api.AssignmentExpressionTree; +import org.sonarsource.slang.api.TextRange; +import org.sonarsource.slang.api.Token; +import org.sonarsource.slang.api.Tree; +import org.sonarsource.slang.api.UnaryExpressionTree; +import org.sonarsource.slang.api.UnaryExpressionTree.Operator; +import org.sonarsource.slang.checks.api.CheckContext; +import org.sonarsource.slang.checks.api.InitContext; +import org.sonarsource.slang.checks.api.SlangCheck; +import org.sonarsource.slang.impl.TextRanges; + +import static java.util.Arrays.asList; +import static org.sonarsource.slang.api.AssignmentExpressionTree.Operator.EQUAL; + +@Rule(key = "S2757") +public class WrongAssignmentOperatorCheck implements SlangCheck { + + private static final List SUSPICIOUS_UNARY_OPERATORS = asList(Operator.NEGATE, Operator.PLUS, Operator.MINUS); + + @Override + public void initialize(InitContext init) { + init.register(AssignmentExpressionTree.class, (ctx, assignment) -> { + Tree rightHandSide = assignment.statementOrExpression(); + if (assignment.operator() != EQUAL || !isSuspiciousUnaryExpression(rightHandSide)) { + return; + } + + List leftHandSideTokens = assignment.leftHandSide().metaData().tokens(); + Token variableLastToken = leftHandSideTokens.get(leftHandSideTokens.size() - 1); + + List allTokens = assignment.metaData().tokens(); + Token operatorToken = allTokens.get(allTokens.indexOf(variableLastToken) + 1); + + Token expressionFirstToken = rightHandSide.metaData().tokens().get(0); + + if (!hasSpacingBetween(operatorToken, expressionFirstToken) && hasSpacingBetween(variableLastToken, operatorToken)) { + TextRange range = TextRanges.merge(asList(operatorToken.textRange(), expressionFirstToken.textRange())); + ctx.reportIssue(range, getMessage(expressionFirstToken, ctx)); + } + }); + } + + private static String getMessage(Token expressionFirstToken, CheckContext aeTree) { + if (isSingleNegationAssignment(expressionFirstToken, aeTree)) { + // For expressions such as "a = b =! c" we want to display the other message + return "Add a space between \"=\" and \"!\" to avoid confusion."; + } + return "Was \"" + expressionFirstToken.text() + "=\" meant instead?"; + } + + private static boolean isSingleNegationAssignment(Token firstToken, CheckContext aeTree) { + return "!".equals(firstToken.text()) && !(aeTree.parent() instanceof AssignmentExpressionTree); + } + + private static boolean hasSpacingBetween(Token firstToken, Token secondToken) { + return firstToken.textRange().end().line() != secondToken.textRange().start().line() + || firstToken.textRange().end().lineOffset() != secondToken.textRange().start().lineOffset(); + } + + private static boolean isSuspiciousUnaryExpression(Tree tree) { + return tree instanceof UnaryExpressionTree unary && SUSPICIOUS_UNARY_OPERATORS.contains(unary.operator()); + } + +} diff --git a/sonar-go-checks/src/main/java/org/sonar/go/checks/complexity/CognitiveComplexity.java b/sonar-go-checks/src/main/java/org/sonar/go/checks/complexity/CognitiveComplexity.java new file mode 100644 index 00000000..fe31388f --- /dev/null +++ b/sonar-go-checks/src/main/java/org/sonar/go/checks/complexity/CognitiveComplexity.java @@ -0,0 +1,185 @@ +/* + * SonarSource Go + * Copyright (C) 2018-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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.go.checks.complexity; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import javax.annotation.Nullable; +import org.sonar.go.checks.utils.ExpressionUtils; +import org.sonarsource.slang.api.BinaryExpressionTree; +import org.sonarsource.slang.api.CatchTree; +import org.sonarsource.slang.api.ClassDeclarationTree; +import org.sonarsource.slang.api.FunctionDeclarationTree; +import org.sonarsource.slang.api.IfTree; +import org.sonarsource.slang.api.LoopTree; +import org.sonarsource.slang.api.MatchTree; +import org.sonarsource.slang.api.Token; +import org.sonarsource.slang.api.Tree; +import org.sonarsource.slang.impl.JumpTreeImpl; +import org.sonarsource.slang.visitors.TreeContext; +import org.sonarsource.slang.visitors.TreeVisitor; + +import static org.sonar.go.checks.utils.ExpressionUtils.isLogicalBinaryExpression; + +public class CognitiveComplexity { + + private List increments = new ArrayList<>(); + + public CognitiveComplexity(Tree root) { + CognitiveComplexityVisitor visitor = new CognitiveComplexityVisitor(); + visitor.scan(new TreeContext(), root); + } + + public int value() { + int total = 0; + for (Increment increment : increments) { + total += increment.nestingLevel + 1; + } + return total; + } + + public List increments() { + return increments; + } + + public static class Increment { + + private final Token token; + private final int nestingLevel; + + private Increment(Token token, int nestingLevel) { + this.token = token; + this.nestingLevel = nestingLevel; + } + + public Token token() { + return token; + } + + public int nestingLevel() { + return nestingLevel; + } + } + + private class CognitiveComplexityVisitor extends TreeVisitor { + + private Set alreadyConsideredOperators = new HashSet<>(); + + private CognitiveComplexityVisitor() { + + // TODO "break" or "continue" with label + + register(LoopTree.class, (ctx, tree) -> incrementWithNesting(tree.keyword(), ctx)); + register(MatchTree.class, (ctx, tree) -> incrementWithNesting(tree.keyword(), ctx)); + register(CatchTree.class, (ctx, tree) -> incrementWithNesting(tree.keyword(), ctx)); + register(JumpTreeImpl.class, (ctx, tree) -> { + if (tree.label() != null) { + incrementWithoutNesting(tree.keyword()); + } + }); + + register(IfTree.class, (ctx, tree) -> { + Tree parent = ctx.ancestors().peek(); + boolean isElseIf = parent instanceof IfTree ifTree && tree == ifTree.elseBranch(); + boolean isTernary = ExpressionUtils.isTernaryOperator(ctx.ancestors(), tree); + if (!isElseIf || isTernary) { + incrementWithNesting(tree.ifKeyword(), ctx); + } + Token elseKeyword = tree.elseKeyword(); + if (elseKeyword != null && !isTernary) { + incrementWithoutNesting(elseKeyword); + } + }); + + register(BinaryExpressionTree.class, (ctx, tree) -> handleBinaryExpressions(tree)); + } + + private void handleBinaryExpressions(BinaryExpressionTree tree) { + if (!isLogicalBinaryExpression(tree) || alreadyConsideredOperators.contains(tree.operatorToken())) { + return; + } + + List operators = new ArrayList<>(); + flattenOperators(tree, operators); + + Token previous = null; + for (Token operator : operators) { + if (previous == null || !previous.text().equals(operator.text())) { + incrementWithoutNesting(operator); + } + previous = operator; + alreadyConsideredOperators.add(operator); + } + } + + private void flattenOperators(BinaryExpressionTree tree, List operators) { + if (isLogicalBinaryExpression(tree.leftOperand())) { + flattenOperators((BinaryExpressionTree) tree.leftOperand(), operators); + } + + operators.add(tree.operatorToken()); + + if (isLogicalBinaryExpression(tree.rightOperand())) { + flattenOperators((BinaryExpressionTree) tree.rightOperand(), operators); + } + } + + private void incrementWithNesting(Token token, TreeContext ctx) { + increment(token, nestingLevel(ctx)); + } + + private void incrementWithoutNesting(Token token) { + increment(token, 0); + } + + private void increment(Token token, int nestingLevel) { + increments.add(new Increment(token, nestingLevel)); + } + + private int nestingLevel(TreeContext ctx) { + int nestingLevel = 0; + boolean isInsideFunction = false; + Iterator ancestors = ctx.ancestors().descendingIterator(); + Tree parent = null; + while (ancestors.hasNext()) { + Tree t = ancestors.next(); + if (t instanceof FunctionDeclarationTree) { + if (isInsideFunction || nestingLevel > 0) { + nestingLevel++; + } + isInsideFunction = true; + } else if ((t instanceof IfTree && !isElseIfBranch(parent, t)) || t instanceof MatchTree || t instanceof LoopTree || t instanceof CatchTree) { + nestingLevel++; + } else if (t instanceof ClassDeclarationTree) { + nestingLevel = 0; + isInsideFunction = false; + } + parent = t; + } + return nestingLevel; + } + + private boolean isElseIfBranch(@Nullable Tree parent, Tree tree) { + return parent instanceof IfTree ifTree && ifTree.elseBranch() == tree; + } + + } + +} diff --git a/sonar-go-checks/src/main/java/org/sonar/go/checks/complexity/package-info.java b/sonar-go-checks/src/main/java/org/sonar/go/checks/complexity/package-info.java new file mode 100644 index 00000000..400aba3a --- /dev/null +++ b/sonar-go-checks/src/main/java/org/sonar/go/checks/complexity/package-info.java @@ -0,0 +1,18 @@ +/* + * SonarSource Go + * Copyright (C) 2018-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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/ + */ +@javax.annotation.ParametersAreNonnullByDefault +package org.sonar.go.checks.complexity; diff --git a/sonar-go-plugin/src/main/java/org/sonar/go/checks/package-info.java b/sonar-go-checks/src/main/java/org/sonar/go/checks/package-info.java similarity index 100% rename from sonar-go-plugin/src/main/java/org/sonar/go/checks/package-info.java rename to sonar-go-checks/src/main/java/org/sonar/go/checks/package-info.java diff --git a/sonar-go-checks/src/main/java/org/sonar/go/checks/utils/ExpressionUtils.java b/sonar-go-checks/src/main/java/org/sonar/go/checks/utils/ExpressionUtils.java new file mode 100644 index 00000000..72c95195 --- /dev/null +++ b/sonar-go-checks/src/main/java/org/sonar/go/checks/utils/ExpressionUtils.java @@ -0,0 +1,134 @@ +/* + * SonarSource Go + * Copyright (C) 2018-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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.go.checks.utils; + +import java.util.Arrays; +import java.util.Deque; +import java.util.List; +import java.util.Optional; +import org.sonarsource.slang.api.BinaryExpressionTree; +import org.sonarsource.slang.api.BlockTree; +import org.sonarsource.slang.api.ExceptionHandlingTree; +import org.sonarsource.slang.api.IdentifierTree; +import org.sonarsource.slang.api.IfTree; +import org.sonarsource.slang.api.LiteralTree; +import org.sonarsource.slang.api.LoopTree; +import org.sonarsource.slang.api.MatchCaseTree; +import org.sonarsource.slang.api.MemberSelectTree; +import org.sonarsource.slang.api.ParenthesizedExpressionTree; +import org.sonarsource.slang.api.PlaceHolderTree; +import org.sonarsource.slang.api.TopLevelTree; +import org.sonarsource.slang.api.Tree; +import org.sonarsource.slang.api.UnaryExpressionTree; + +import static org.sonarsource.slang.api.BinaryExpressionTree.Operator.CONDITIONAL_AND; +import static org.sonarsource.slang.api.BinaryExpressionTree.Operator.CONDITIONAL_OR; + +public class ExpressionUtils { + private static final String TRUE_LITERAL = "true"; + private static final String FALSE_LITERAL = "false"; + private static final List BOOLEAN_LITERALS = Arrays.asList(TRUE_LITERAL, FALSE_LITERAL); + + private ExpressionUtils() { + } + + public static boolean isBooleanLiteral(Tree tree) { + return tree instanceof LiteralTree literalTree && BOOLEAN_LITERALS.contains(literalTree.value()); + } + + public static boolean isFalseValueLiteral(Tree originalTree) { + Tree tree = skipParentheses(originalTree); + return (tree instanceof LiteralTree literalTree && FALSE_LITERAL.equals(literalTree.value())) + || (isNegation(tree) && isTrueValueLiteral(((UnaryExpressionTree) tree).operand())); + } + + public static boolean isTrueValueLiteral(Tree originalTree) { + Tree tree = skipParentheses(originalTree); + return (tree instanceof LiteralTree literalTree && TRUE_LITERAL.equals(literalTree.value())) + || (isNegation(tree) && isFalseValueLiteral(((UnaryExpressionTree) tree).operand())); + } + + public static boolean isNegation(Tree tree) { + return tree instanceof UnaryExpressionTree unary && unary.operator() == UnaryExpressionTree.Operator.NEGATE; + } + + public static boolean isBinaryOperation(Tree tree, BinaryExpressionTree.Operator operator) { + return tree instanceof BinaryExpressionTree binaryExpressionTree && binaryExpressionTree.operator() == operator; + } + + public static boolean isLogicalBinaryExpression(Tree tree) { + return isBinaryOperation(tree, CONDITIONAL_AND) || isBinaryOperation(tree, CONDITIONAL_OR); + } + + public static Tree skipParentheses(Tree tree) { + Tree result = tree; + while (result instanceof ParenthesizedExpressionTree parenthesizedExpressionTree) { + result = parenthesizedExpressionTree.expression(); + } + return result; + } + + public static boolean containsPlaceHolder(Tree tree) { + return tree.descendants().anyMatch(PlaceHolderTree.class::isInstance); + } + + public static boolean isTernaryOperator(Deque ancestors, Tree tree) { + if (!isIfWithElse(tree)) { + return false; + } + Tree child = tree; + for (Tree ancestor : ancestors) { + if (ancestor instanceof BlockTree || ancestor instanceof ExceptionHandlingTree || ancestor instanceof TopLevelTree || + isBranchOfLoopOrCaseOrIfWithoutElse(ancestor, child)) { + break; + } + if (!isBranchOfIf(ancestor, child)) { + return tree.descendants().noneMatch(BlockTree.class::isInstance); + } + child = ancestor; + } + return false; + } + + private static boolean isIfWithElse(Tree tree) { + return tree instanceof IfTree ifTree && ifTree.elseBranch() != null; + } + + private static boolean isBranchOfLoopOrCaseOrIfWithoutElse(Tree parent, Tree child) { + return (parent instanceof LoopTree loopTree && child == loopTree.body()) || + (parent instanceof MatchCaseTree matchCaseTree && child == matchCaseTree.body()) || + (isBranchOfIf(parent, child) && ((IfTree) parent).elseBranch() == null); + } + + private static boolean isBranchOfIf(Tree parent, Tree child) { + if (parent instanceof IfTree ifTree) { + return child == ifTree.thenBranch() || child == ifTree.elseBranch(); + } + return false; + } + + public static Optional getMemberSelectOrIdentifierName(Tree tree) { + if (tree instanceof IdentifierTree identifierTree) { + return Optional.of(identifierTree.name()); + } else if (tree instanceof MemberSelectTree memberSelectTree) { + return Optional.of(memberSelectTree.identifier().name()); + } else { + return Optional.empty(); + } + } + +} diff --git a/sonar-go-checks/src/main/java/org/sonar/go/checks/utils/FunctionUtils.java b/sonar-go-checks/src/main/java/org/sonar/go/checks/utils/FunctionUtils.java new file mode 100644 index 00000000..804ca683 --- /dev/null +++ b/sonar-go-checks/src/main/java/org/sonar/go/checks/utils/FunctionUtils.java @@ -0,0 +1,95 @@ +/* + * SonarSource Go + * Copyright (C) 2018-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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.go.checks.utils; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import org.sonarsource.slang.api.FunctionDeclarationTree; +import org.sonarsource.slang.api.FunctionInvocationTree; +import org.sonarsource.slang.api.IdentifierTree; +import org.sonarsource.slang.api.MemberSelectTree; +import org.sonarsource.slang.api.ModifierTree; +import org.sonarsource.slang.api.StringLiteralTree; +import org.sonarsource.slang.api.Tree; + +import static org.sonar.go.checks.utils.ExpressionUtils.getMemberSelectOrIdentifierName; +import static org.sonarsource.slang.api.ModifierTree.Kind.OVERRIDE; +import static org.sonarsource.slang.api.ModifierTree.Kind.PRIVATE; + +public class FunctionUtils { + + private FunctionUtils() { + } + + public static boolean isPrivateMethod(FunctionDeclarationTree method) { + return hasModifierMethod(method, PRIVATE); + + } + + public static boolean isOverrideMethod(FunctionDeclarationTree method) { + return hasModifierMethod(method, OVERRIDE); + } + + public static boolean hasModifierMethod(FunctionDeclarationTree method, ModifierTree.Kind kind) { + return method.modifiers().stream() + .filter(ModifierTree.class::isInstance) + .map(ModifierTree.class::cast) + .anyMatch(modifier -> modifier.kind() == kind); + } + + public static boolean hasFunctionCallNameIgnoreCase(FunctionInvocationTree tree, String name) { + return getFunctionInvocationName(tree).filter(name::equalsIgnoreCase).isPresent(); + } + + public static Set getStringsTokens(FunctionDeclarationTree functionDeclarationTree, String delimitersRegex) { + Set stringLiteralTokens = new HashSet<>(); + functionDeclarationTree.descendants() + .filter(StringLiteralTree.class::isInstance) + .map(StringLiteralTree.class::cast) + .map(StringLiteralTree::content) + .forEach(literal -> stringLiteralTokens.addAll(Arrays.asList(literal.split(delimitersRegex)))); + return stringLiteralTokens; + } + + private static Optional getFunctionInvocationName(FunctionInvocationTree tree) { + return getMemberSelectOrIdentifierName(tree.memberSelect()); + } + + public static boolean hasFunctionCallFullNameIgnoreCase(FunctionInvocationTree tree, String... names) { + return hasFunctionCallFullNameIgnoreCaseHelper(tree.memberSelect(), Arrays.asList(names)); + } + + private static boolean hasFunctionCallFullNameIgnoreCaseHelper(Tree tree, List names) { + if (tree instanceof IdentifierTree identifierTree) { + return names.size() == 1 && identifierTree.name().equalsIgnoreCase(names.get(0)); + } else if (tree instanceof MemberSelectTree memberSelectTree) { + return names.size() > 1 + && memberSelectTree.identifier().name().equalsIgnoreCase(names.get(names.size() - 1)) + && hasFunctionCallFullNameIgnoreCaseHelper(memberSelectTree.expression(), dropLastElement(names)); + } else { + // Any other node (native, ...): we don't known anything about them! + return false; + } + } + + private static List dropLastElement(List list) { + return list.subList(0, list.size() - 1); + } +} diff --git a/sonar-go-checks/src/main/java/org/sonar/go/checks/utils/package-info.java b/sonar-go-checks/src/main/java/org/sonar/go/checks/utils/package-info.java new file mode 100644 index 00000000..6854cd48 --- /dev/null +++ b/sonar-go-checks/src/main/java/org/sonar/go/checks/utils/package-info.java @@ -0,0 +1,18 @@ +/* + * SonarSource Go + * Copyright (C) 2018-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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/ + */ +@javax.annotation.ParametersAreNonnullByDefault +package org.sonar.go.checks.utils; diff --git a/sonar-go-checks/src/test/java/org/sonar/go/checks/AllBranchesIdenticalCheckTest.java b/sonar-go-checks/src/test/java/org/sonar/go/checks/AllBranchesIdenticalCheckTest.java new file mode 100644 index 00000000..a650abe8 --- /dev/null +++ b/sonar-go-checks/src/test/java/org/sonar/go/checks/AllBranchesIdenticalCheckTest.java @@ -0,0 +1,28 @@ +/* + * SonarSource Go + * Copyright (C) 2018-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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.go.checks; + +import org.junit.jupiter.api.Test; + +class AllBranchesIdenticalCheckTest { + + @Test + void test() { + SlangVerifier.verify("AllBranchesIdentical.slang", new AllBranchesIdenticalCheck()); + } + +} diff --git a/sonar-go-checks/src/test/java/org/sonar/go/checks/BadFunctionNameCheckTest.java b/sonar-go-checks/src/test/java/org/sonar/go/checks/BadFunctionNameCheckTest.java new file mode 100644 index 00000000..528261db --- /dev/null +++ b/sonar-go-checks/src/test/java/org/sonar/go/checks/BadFunctionNameCheckTest.java @@ -0,0 +1,34 @@ +/* + * SonarSource Go + * Copyright (C) 2018-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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.go.checks; + +import org.junit.jupiter.api.Test; + +class BadFunctionNameCheckTest { + + @Test + void test() { + SlangVerifier.verify("BadFunctionName.slang", new BadFunctionNameCheck()); + } + + @Test + void test_upper_case() { + BadFunctionNameCheck check = new BadFunctionNameCheck(); + check.format = "^[A-Z]*$"; + SlangVerifier.verify("BadFunctionName.uppercase.slang", check); + } +} diff --git a/sonar-go-checks/src/test/java/org/sonar/go/checks/BooleanInversionCheckTest.java b/sonar-go-checks/src/test/java/org/sonar/go/checks/BooleanInversionCheckTest.java new file mode 100644 index 00000000..3244cc1c --- /dev/null +++ b/sonar-go-checks/src/test/java/org/sonar/go/checks/BooleanInversionCheckTest.java @@ -0,0 +1,28 @@ +/* + * SonarSource Go + * Copyright (C) 2018-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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.go.checks; + +import org.junit.jupiter.api.Test; + +class BooleanInversionCheckTest { + + @Test + void test() { + SlangVerifier.verify("BooleanInversion.slang", new BooleanInversionCheck()); + } + +} diff --git a/sonar-go-checks/src/test/java/org/sonar/go/checks/BooleanLiteralCheckTest.java b/sonar-go-checks/src/test/java/org/sonar/go/checks/BooleanLiteralCheckTest.java new file mode 100644 index 00000000..24c6aead --- /dev/null +++ b/sonar-go-checks/src/test/java/org/sonar/go/checks/BooleanLiteralCheckTest.java @@ -0,0 +1,26 @@ +/* + * SonarSource Go + * Copyright (C) 2018-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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.go.checks; + +import org.junit.jupiter.api.Test; + +class BooleanLiteralCheckTest { + @Test + void test() { + SlangVerifier.verify("BooleanLiteral.slang", new BooleanLiteralCheck()); + } +} diff --git a/sonar-go-plugin/src/test/java/org/sonar/go/checks/CodeAfterJumpGoCheckTest.java b/sonar-go-checks/src/test/java/org/sonar/go/checks/CodeAfterJumpGoCheckTest.java similarity index 100% rename from sonar-go-plugin/src/test/java/org/sonar/go/checks/CodeAfterJumpGoCheckTest.java rename to sonar-go-checks/src/test/java/org/sonar/go/checks/CodeAfterJumpGoCheckTest.java diff --git a/sonar-go-plugin/src/test/java/org/sonar/go/checks/DuplicateBranchGoCheckTest.java b/sonar-go-checks/src/test/java/org/sonar/go/checks/DuplicateBranchGoCheckTest.java similarity index 100% rename from sonar-go-plugin/src/test/java/org/sonar/go/checks/DuplicateBranchGoCheckTest.java rename to sonar-go-checks/src/test/java/org/sonar/go/checks/DuplicateBranchGoCheckTest.java diff --git a/sonar-go-checks/src/test/java/org/sonar/go/checks/DuplicatedFunctionImplementationCheckTest.java b/sonar-go-checks/src/test/java/org/sonar/go/checks/DuplicatedFunctionImplementationCheckTest.java new file mode 100644 index 00000000..8169fbe4 --- /dev/null +++ b/sonar-go-checks/src/test/java/org/sonar/go/checks/DuplicatedFunctionImplementationCheckTest.java @@ -0,0 +1,28 @@ +/* + * SonarSource Go + * Copyright (C) 2018-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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.go.checks; + +import org.junit.jupiter.api.Test; + +class DuplicatedFunctionImplementationCheckTest { + + @Test + void test() { + SlangVerifier.verify("DuplicatedFunctionImplementation.slang", new DuplicatedFunctionImplementationCheck()); + } + +} diff --git a/sonar-go-checks/src/test/java/org/sonar/go/checks/ElseIfWithoutElseCheckTest.java b/sonar-go-checks/src/test/java/org/sonar/go/checks/ElseIfWithoutElseCheckTest.java new file mode 100644 index 00000000..446cfdc9 --- /dev/null +++ b/sonar-go-checks/src/test/java/org/sonar/go/checks/ElseIfWithoutElseCheckTest.java @@ -0,0 +1,26 @@ +/* + * SonarSource Go + * Copyright (C) 2018-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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.go.checks; + +import org.junit.jupiter.api.Test; + +class ElseIfWithoutElseCheckTest { + @Test + void test() { + SlangVerifier.verify("ElseIfWithoutElse.slang", new ElseIfWithoutElseCheck()); + } +} diff --git a/sonar-go-checks/src/test/java/org/sonar/go/checks/EmptyBlockCheckTest.java b/sonar-go-checks/src/test/java/org/sonar/go/checks/EmptyBlockCheckTest.java new file mode 100644 index 00000000..cbaf76a9 --- /dev/null +++ b/sonar-go-checks/src/test/java/org/sonar/go/checks/EmptyBlockCheckTest.java @@ -0,0 +1,28 @@ +/* + * SonarSource Go + * Copyright (C) 2018-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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.go.checks; + +import org.junit.jupiter.api.Test; + +class EmptyBlockCheckTest { + + @Test + void test() { + SlangVerifier.verify("EmptyBlock.slang", new EmptyBlockCheck()); + } + +} diff --git a/sonar-go-checks/src/test/java/org/sonar/go/checks/EmptyCommentCheckTest.java b/sonar-go-checks/src/test/java/org/sonar/go/checks/EmptyCommentCheckTest.java new file mode 100644 index 00000000..b5be5336 --- /dev/null +++ b/sonar-go-checks/src/test/java/org/sonar/go/checks/EmptyCommentCheckTest.java @@ -0,0 +1,28 @@ +/* + * SonarSource Go + * Copyright (C) 2018-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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.go.checks; + +import org.junit.jupiter.api.Test; + +class EmptyCommentCheckTest { + + @Test + void test() { + SlangVerifier.verify("EmptyComment.slang", new EmptyCommentCheck()); + } + +} diff --git a/sonar-go-checks/src/test/java/org/sonar/go/checks/EmptyFunctionCheckTest.java b/sonar-go-checks/src/test/java/org/sonar/go/checks/EmptyFunctionCheckTest.java new file mode 100644 index 00000000..c3fed702 --- /dev/null +++ b/sonar-go-checks/src/test/java/org/sonar/go/checks/EmptyFunctionCheckTest.java @@ -0,0 +1,28 @@ +/* + * SonarSource Go + * Copyright (C) 2018-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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.go.checks; + +import org.junit.jupiter.api.Test; + +class EmptyFunctionCheckTest { + + @Test + void test() { + SlangVerifier.verify("EmptyFunction.slang", new EmptyFunctionCheck()); + } + +} diff --git a/sonar-go-checks/src/test/java/org/sonar/go/checks/FileHeaderCheckTest.java b/sonar-go-checks/src/test/java/org/sonar/go/checks/FileHeaderCheckTest.java new file mode 100644 index 00000000..51fb643d --- /dev/null +++ b/sonar-go-checks/src/test/java/org/sonar/go/checks/FileHeaderCheckTest.java @@ -0,0 +1,56 @@ +/* + * SonarSource Go + * Copyright (C) 2018-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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.go.checks; + +import org.junit.jupiter.api.Test; + +class FileHeaderCheckTest { + private FileHeaderCheck check = new FileHeaderCheck(); + + @Test + void test() { + check.headerFormat = "// copyright 2018"; + SlangVerifier.verify("fileheader/Noncompliant.slang", check); + SlangVerifier.verifyNoIssue("fileheader/Compliant.slang", check); + } + + @Test + void test_regex() { + check.headerFormat = "// copyright 20\\d\\d"; + check.isRegularExpression = true; + SlangVerifier.verify("fileheader/Noncompliant.slang", check); + SlangVerifier.verifyNoIssue("fileheader/Compliant.slang", check); + } + + @Test + void test_multiline() { + check.headerFormat = """ + /* + * SonarSource SLang + * Copyright (C) 1999-2001 SonarSource SA + * mailto:info AT sonarsource DOT com + */"""; + SlangVerifier.verifyNoIssue("fileheader/Multiline.slang", check); + } + + @Test + void test_no_first_line() { + check.headerFormat = "// copyright 20\\d\\d"; + check.isRegularExpression = true; + SlangVerifier.verify("fileheader/NoFirstLine.slang", check); + } +} diff --git a/sonar-go-checks/src/test/java/org/sonar/go/checks/FixMeCommentCheckTest.java b/sonar-go-checks/src/test/java/org/sonar/go/checks/FixMeCommentCheckTest.java new file mode 100644 index 00000000..38610e3a --- /dev/null +++ b/sonar-go-checks/src/test/java/org/sonar/go/checks/FixMeCommentCheckTest.java @@ -0,0 +1,28 @@ +/* + * SonarSource Go + * Copyright (C) 2018-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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.go.checks; + +import org.junit.jupiter.api.Test; + +class FixMeCommentCheckTest { + + @Test + void test() { + SlangVerifier.verify("FixMeComment.slang", new FixMeCommentCheck()); + } + +} diff --git a/sonar-go-checks/src/test/java/org/sonar/go/checks/FunctionCognitiveComplexityCheckTest.java b/sonar-go-checks/src/test/java/org/sonar/go/checks/FunctionCognitiveComplexityCheckTest.java new file mode 100644 index 00000000..f4820572 --- /dev/null +++ b/sonar-go-checks/src/test/java/org/sonar/go/checks/FunctionCognitiveComplexityCheckTest.java @@ -0,0 +1,30 @@ +/* + * SonarSource Go + * Copyright (C) 2018-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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.go.checks; + +import org.junit.jupiter.api.Test; + +class FunctionCognitiveComplexityCheckTest { + + @Test + void test() { + FunctionCognitiveComplexityCheck check = new FunctionCognitiveComplexityCheck(); + check.threshold = 4; + SlangVerifier.verify("FunctionCognitiveComplexity.slang", check); + } + +} diff --git a/sonar-go-plugin/src/test/java/org/sonar/go/checks/GoVerifier.java b/sonar-go-checks/src/test/java/org/sonar/go/checks/GoVerifier.java similarity index 97% rename from sonar-go-plugin/src/test/java/org/sonar/go/checks/GoVerifier.java rename to sonar-go-checks/src/test/java/org/sonar/go/checks/GoVerifier.java index 6d07fdb5..525aac24 100644 --- a/sonar-go-plugin/src/test/java/org/sonar/go/checks/GoVerifier.java +++ b/sonar-go-checks/src/test/java/org/sonar/go/checks/GoVerifier.java @@ -52,6 +52,10 @@ public static void verify(ASTConverter converter, Path path, SlangCheck check) { createVerifier(converter, path, check).assertOneOrMoreIssues(); } + public static void verifyNoIssue(String fileName, SlangCheck check) { + verifyNoIssue(TestGoConverter.GO_CONVERTER, BASE_DIR.resolve(fileName), check); + } + public static void verifyNoIssue(ASTConverter converter, Path path, SlangCheck check) { createVerifier(converter, path, check).assertNoIssues(); } diff --git a/sonar-go-checks/src/test/java/org/sonar/go/checks/HardcodedCredentialsCheckTest.java b/sonar-go-checks/src/test/java/org/sonar/go/checks/HardcodedCredentialsCheckTest.java new file mode 100644 index 00000000..85ffbc9b --- /dev/null +++ b/sonar-go-checks/src/test/java/org/sonar/go/checks/HardcodedCredentialsCheckTest.java @@ -0,0 +1,28 @@ +/* + * SonarSource Go + * Copyright (C) 2018-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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.go.checks; + +import org.junit.jupiter.api.Test; + +class HardcodedCredentialsCheckTest { + + @Test + void test() { + SlangVerifier.verify("HardcodedCredentials.slang", new HardcodedCredentialsCheck()); + } + +} diff --git a/sonar-go-checks/src/test/java/org/sonar/go/checks/HardcodedIpCheckTest.java b/sonar-go-checks/src/test/java/org/sonar/go/checks/HardcodedIpCheckTest.java new file mode 100644 index 00000000..cf852527 --- /dev/null +++ b/sonar-go-checks/src/test/java/org/sonar/go/checks/HardcodedIpCheckTest.java @@ -0,0 +1,27 @@ +/* + * SonarSource Go + * Copyright (C) 2018-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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.go.checks; + +import org.junit.jupiter.api.Test; + +class HardcodedIpCheckTest { + @Test + void test() { + SlangVerifier.verify("HardcodedIp.slang", new HardcodedIpCheck()); + } + +} diff --git a/sonar-go-checks/src/test/java/org/sonar/go/checks/IdenticalBinaryOperandCheckTest.java b/sonar-go-checks/src/test/java/org/sonar/go/checks/IdenticalBinaryOperandCheckTest.java new file mode 100644 index 00000000..38c9970c --- /dev/null +++ b/sonar-go-checks/src/test/java/org/sonar/go/checks/IdenticalBinaryOperandCheckTest.java @@ -0,0 +1,28 @@ +/* + * SonarSource Go + * Copyright (C) 2018-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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.go.checks; + +import org.junit.jupiter.api.Test; + +class IdenticalBinaryOperandCheckTest { + + @Test + void test() { + SlangVerifier.verify("IdenticalBinaryOperand.slang", new IdenticalBinaryOperandCheck()); + } + +} diff --git a/sonar-go-checks/src/test/java/org/sonar/go/checks/IdenticalConditionsCheckTest.java b/sonar-go-checks/src/test/java/org/sonar/go/checks/IdenticalConditionsCheckTest.java new file mode 100644 index 00000000..113a8d14 --- /dev/null +++ b/sonar-go-checks/src/test/java/org/sonar/go/checks/IdenticalConditionsCheckTest.java @@ -0,0 +1,28 @@ +/* + * SonarSource Go + * Copyright (C) 2018-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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.go.checks; + +import org.junit.jupiter.api.Test; + +class IdenticalConditionsCheckTest { + + @Test + void test() { + SlangVerifier.verify("IdenticalConditions.slang", new IdenticalConditionsCheck()); + } + +} diff --git a/sonar-go-checks/src/test/java/org/sonar/go/checks/IfConditionalAlwaysTrueOrFalseCheckTest.java b/sonar-go-checks/src/test/java/org/sonar/go/checks/IfConditionalAlwaysTrueOrFalseCheckTest.java new file mode 100644 index 00000000..70414a7f --- /dev/null +++ b/sonar-go-checks/src/test/java/org/sonar/go/checks/IfConditionalAlwaysTrueOrFalseCheckTest.java @@ -0,0 +1,28 @@ +/* + * SonarSource Go + * Copyright (C) 2018-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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.go.checks; + +import org.junit.jupiter.api.Test; + +class IfConditionalAlwaysTrueOrFalseCheckTest { + + @Test + void test() { + SlangVerifier.verify("IfConditionalAlwaysTrueOrFalse.slang", new IfConditionalAlwaysTrueOrFalseCheck()); + } + +} diff --git a/sonar-go-checks/src/test/java/org/sonar/go/checks/MatchCaseTooBigCheckTest.java b/sonar-go-checks/src/test/java/org/sonar/go/checks/MatchCaseTooBigCheckTest.java new file mode 100644 index 00000000..464160d1 --- /dev/null +++ b/sonar-go-checks/src/test/java/org/sonar/go/checks/MatchCaseTooBigCheckTest.java @@ -0,0 +1,36 @@ +/* + * SonarSource Go + * Copyright (C) 2018-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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.go.checks; + +import org.junit.jupiter.api.Test; + +class MatchCaseTooBigCheckTest { + + private MatchCaseTooBigCheck check = new MatchCaseTooBigCheck(); + + @Test + void max_5() { + check.max = 5; + SlangVerifier.verify("MatchCaseTooBig_5.slang", check); + } + + @Test + void max_3() { + check.max = 3; + SlangVerifier.verify("MatchCaseTooBig_3.slang", check); + } +} diff --git a/sonar-go-checks/src/test/java/org/sonar/go/checks/MatchWithoutElseCheckTest.java b/sonar-go-checks/src/test/java/org/sonar/go/checks/MatchWithoutElseCheckTest.java new file mode 100644 index 00000000..d152bb27 --- /dev/null +++ b/sonar-go-checks/src/test/java/org/sonar/go/checks/MatchWithoutElseCheckTest.java @@ -0,0 +1,28 @@ +/* + * SonarSource Go + * Copyright (C) 2018-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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.go.checks; + +import org.junit.jupiter.api.Test; + +class MatchWithoutElseCheckTest { + + @Test + void test() { + SlangVerifier.verify("MatchWithoutElse.slang", new MatchWithoutElseCheck()); + } + +} diff --git a/sonar-go-checks/src/test/java/org/sonar/go/checks/NestedMatchCheckTest.java b/sonar-go-checks/src/test/java/org/sonar/go/checks/NestedMatchCheckTest.java new file mode 100644 index 00000000..f66f06ad --- /dev/null +++ b/sonar-go-checks/src/test/java/org/sonar/go/checks/NestedMatchCheckTest.java @@ -0,0 +1,26 @@ +/* + * SonarSource Go + * Copyright (C) 2018-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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.go.checks; + +import org.junit.jupiter.api.Test; + +class NestedMatchCheckTest { + @Test + void test() { + SlangVerifier.verify("NestedMatch.slang", new NestedMatchCheck()); + } +} diff --git a/sonar-go-checks/src/test/java/org/sonar/go/checks/OctalValuesCheckTest.java b/sonar-go-checks/src/test/java/org/sonar/go/checks/OctalValuesCheckTest.java new file mode 100644 index 00000000..257fa509 --- /dev/null +++ b/sonar-go-checks/src/test/java/org/sonar/go/checks/OctalValuesCheckTest.java @@ -0,0 +1,28 @@ +/* + * SonarSource Go + * Copyright (C) 2018-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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.go.checks; + +import org.junit.jupiter.api.Test; + +class OctalValuesCheckTest { + + @Test + void test() { + SlangVerifier.verify("OctalValues.slang", new OctalValuesCheck()); + } + +} diff --git a/sonar-go-plugin/src/test/java/org/sonar/go/checks/OneStatementPerLineGoCheckTest.java b/sonar-go-checks/src/test/java/org/sonar/go/checks/OneStatementPerLineGoCheckTest.java similarity index 100% rename from sonar-go-plugin/src/test/java/org/sonar/go/checks/OneStatementPerLineGoCheckTest.java rename to sonar-go-checks/src/test/java/org/sonar/go/checks/OneStatementPerLineGoCheckTest.java diff --git a/sonar-go-checks/src/test/java/org/sonar/go/checks/RedundantParenthesesCheckTest.java b/sonar-go-checks/src/test/java/org/sonar/go/checks/RedundantParenthesesCheckTest.java new file mode 100644 index 00000000..699c2c03 --- /dev/null +++ b/sonar-go-checks/src/test/java/org/sonar/go/checks/RedundantParenthesesCheckTest.java @@ -0,0 +1,28 @@ +/* + * SonarSource Go + * Copyright (C) 2018-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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.go.checks; + +import org.junit.jupiter.api.Test; + +class RedundantParenthesesCheckTest { + + @Test + void test() { + SlangVerifier.verify("RedundantParentheses.slang", new RedundantParenthesesCheck()); + } + +} diff --git a/sonar-go-checks/src/test/java/org/sonar/go/checks/SelfAssignmentCheckTest.java b/sonar-go-checks/src/test/java/org/sonar/go/checks/SelfAssignmentCheckTest.java new file mode 100644 index 00000000..2df0556e --- /dev/null +++ b/sonar-go-checks/src/test/java/org/sonar/go/checks/SelfAssignmentCheckTest.java @@ -0,0 +1,28 @@ +/* + * SonarSource Go + * Copyright (C) 2018-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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.go.checks; + +import org.junit.jupiter.api.Test; + +class SelfAssignmentCheckTest { + + @Test + void test() { + SlangVerifier.verify("SelfAssignment.slang", new SelfAssignmentCheck()); + } + +} diff --git a/sonar-go-checks/src/test/java/org/sonar/go/checks/SlangVerifier.java b/sonar-go-checks/src/test/java/org/sonar/go/checks/SlangVerifier.java new file mode 100644 index 00000000..9be11191 --- /dev/null +++ b/sonar-go-checks/src/test/java/org/sonar/go/checks/SlangVerifier.java @@ -0,0 +1,38 @@ +/* + * SonarSource Go + * Copyright (C) 2018-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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.go.checks; + +import java.nio.file.Path; +import java.nio.file.Paths; +import org.sonarsource.slang.api.ASTConverter; +import org.sonarsource.slang.checks.api.SlangCheck; +import org.sonarsource.slang.parser.SLangConverter; + +public class SlangVerifier { + + private static final Path BASE_DIR = Paths.get("src", "test", "resources", "checks"); + private static final ASTConverter CONVERTER = new SLangConverter(); + + public static void verify(String fileName, SlangCheck check) { + org.sonarsource.slang.testing.Verifier.verify(CONVERTER, BASE_DIR.resolve(fileName), check); + } + + public static void verifyNoIssue(String fileName, SlangCheck check) { + org.sonarsource.slang.testing.Verifier.verifyNoIssue(CONVERTER, BASE_DIR.resolve(fileName), check); + } + +} diff --git a/sonar-go-checks/src/test/java/org/sonar/go/checks/StringLiteralDuplicatedCheckTest.java b/sonar-go-checks/src/test/java/org/sonar/go/checks/StringLiteralDuplicatedCheckTest.java new file mode 100644 index 00000000..6ade1ba8 --- /dev/null +++ b/sonar-go-checks/src/test/java/org/sonar/go/checks/StringLiteralDuplicatedCheckTest.java @@ -0,0 +1,35 @@ +/* + * SonarSource Go + * Copyright (C) 2018-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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.go.checks; + +import org.junit.jupiter.api.Test; + +class StringLiteralDuplicatedCheckTest { + + @Test + void test() { + SlangVerifier.verify("StringLiteralDuplicated.slang", new StringLiteralDuplicatedCheck()); + } + + @Test + void test_threshold_4() { + StringLiteralDuplicatedCheck check = new StringLiteralDuplicatedCheck(); + check.threshold = 4; + SlangVerifier.verify("StringLiteralDuplicated.threshold_4.slang", check); + } + +} diff --git a/sonar-go-checks/src/test/java/org/sonar/go/checks/TodoCommentCheckTest.java b/sonar-go-checks/src/test/java/org/sonar/go/checks/TodoCommentCheckTest.java new file mode 100644 index 00000000..a4b558ef --- /dev/null +++ b/sonar-go-checks/src/test/java/org/sonar/go/checks/TodoCommentCheckTest.java @@ -0,0 +1,28 @@ +/* + * SonarSource Go + * Copyright (C) 2018-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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.go.checks; + +import org.junit.jupiter.api.Test; + +class TodoCommentCheckTest { + + @Test + void test() { + SlangVerifier.verify("TodoComment.slang", new TodoCommentCheck()); + } + +} diff --git a/sonar-go-checks/src/test/java/org/sonar/go/checks/TooComplexExpressionCheckTest.java b/sonar-go-checks/src/test/java/org/sonar/go/checks/TooComplexExpressionCheckTest.java new file mode 100644 index 00000000..b68d822c --- /dev/null +++ b/sonar-go-checks/src/test/java/org/sonar/go/checks/TooComplexExpressionCheckTest.java @@ -0,0 +1,35 @@ +/* + * SonarSource Go + * Copyright (C) 2018-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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.go.checks; + +import org.junit.jupiter.api.Test; + +class TooComplexExpressionCheckTest { + + @Test + void test_max_3() { + SlangVerifier.verify("TooComplexExpression_3.slang", new TooComplexExpressionCheck()); + } + + @Test + void test_max_2() { + var check = new TooComplexExpressionCheck(); + check.max = 2; + SlangVerifier.verify("TooComplexExpression_2.slang", check); + } + +} diff --git a/sonar-go-checks/src/test/java/org/sonar/go/checks/TooDeeplyNestedStatementsCheckTest.java b/sonar-go-checks/src/test/java/org/sonar/go/checks/TooDeeplyNestedStatementsCheckTest.java new file mode 100644 index 00000000..fca03446 --- /dev/null +++ b/sonar-go-checks/src/test/java/org/sonar/go/checks/TooDeeplyNestedStatementsCheckTest.java @@ -0,0 +1,35 @@ +/* + * SonarSource Go + * Copyright (C) 2018-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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.go.checks; + +import org.junit.jupiter.api.Test; + +class TooDeeplyNestedStatementsCheckTest { + + @Test + void test() { + SlangVerifier.verify("TooDeeplyNestedStatements.slang", new TooDeeplyNestedStatementsCheck()); + } + + @Test + void test_max_2() { + TooDeeplyNestedStatementsCheck check = new TooDeeplyNestedStatementsCheck(); + check.max = 2; + SlangVerifier.verify("TooDeeplyNestedStatements.max_2.slang", check); + } + +} diff --git a/sonar-go-checks/src/test/java/org/sonar/go/checks/TooLongFunctionCheckTest.java b/sonar-go-checks/src/test/java/org/sonar/go/checks/TooLongFunctionCheckTest.java new file mode 100644 index 00000000..8b76aa4f --- /dev/null +++ b/sonar-go-checks/src/test/java/org/sonar/go/checks/TooLongFunctionCheckTest.java @@ -0,0 +1,37 @@ +/* + * SonarSource Go + * Copyright (C) 2018-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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.go.checks; + +import org.junit.jupiter.api.Test; + +class TooLongFunctionCheckTest { + + TooLongFunctionCheck check = new TooLongFunctionCheck(); + + @Test + void max_3() { + check.max = 3; + SlangVerifier.verify("TooLongFunction_3.slang", check); + } + + @Test + void max_4() { + check.max = 4; + SlangVerifier.verify("TooLongFunction_4.slang", check); + } + +} diff --git a/sonar-go-checks/src/test/java/org/sonar/go/checks/TooLongLineCheckTest.java b/sonar-go-checks/src/test/java/org/sonar/go/checks/TooLongLineCheckTest.java new file mode 100644 index 00000000..b82cc07d --- /dev/null +++ b/sonar-go-checks/src/test/java/org/sonar/go/checks/TooLongLineCheckTest.java @@ -0,0 +1,36 @@ +/* + * SonarSource Go + * Copyright (C) 2018-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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.go.checks; + +import org.junit.jupiter.api.Test; + +class TooLongLineCheckTest { + + private TooLongLineCheck check = new TooLongLineCheck(); + + @Test + void max_120() { + check.maximumLineLength = 120; + SlangVerifier.verify("TooLongLine_120.slang", check); + } + + @Test + void max_40() { + check.maximumLineLength = 40; + SlangVerifier.verify("TooLongLine_40.slang", check); + } +} diff --git a/sonar-go-checks/src/test/java/org/sonar/go/checks/TooManyCasesCheckTest.java b/sonar-go-checks/src/test/java/org/sonar/go/checks/TooManyCasesCheckTest.java new file mode 100644 index 00000000..61b5d8ef --- /dev/null +++ b/sonar-go-checks/src/test/java/org/sonar/go/checks/TooManyCasesCheckTest.java @@ -0,0 +1,37 @@ +/* + * SonarSource Go + * Copyright (C) 2018-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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.go.checks; + +import org.junit.jupiter.api.Test; + +class TooManyCasesCheckTest { + + private TooManyCasesCheck check = new TooManyCasesCheck(); + + @Test + void test_3() { + check.maximum = 3; + SlangVerifier.verify("TooManyCases_3.slang", check); + } + + @Test + void test_4() { + check.maximum = 4; + SlangVerifier.verify("TooManyCases_4.slang", check); + } + +} diff --git a/sonar-go-checks/src/test/java/org/sonar/go/checks/TooManyLinesOfCodeFileCheckTest.java b/sonar-go-checks/src/test/java/org/sonar/go/checks/TooManyLinesOfCodeFileCheckTest.java new file mode 100644 index 00000000..d7d916f9 --- /dev/null +++ b/sonar-go-checks/src/test/java/org/sonar/go/checks/TooManyLinesOfCodeFileCheckTest.java @@ -0,0 +1,37 @@ +/* + * SonarSource Go + * Copyright (C) 2018-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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.go.checks; + +import org.junit.jupiter.api.Test; + +class TooManyLinesOfCodeFileCheckTest { + + TooManyLinesOfCodeFileCheck check = new TooManyLinesOfCodeFileCheck(); + + @Test + void max_4() { + check.max = 4; + SlangVerifier.verify("TooManyLinesOfCodeFile.max_4.slang", check); + } + + @Test + void max_5() { + check.max = 5; + SlangVerifier.verifyNoIssue("TooManyLinesOfCodeFile.max_5.slang", check); + } + +} diff --git a/sonar-go-checks/src/test/java/org/sonar/go/checks/TooManyParametersCheckTest.java b/sonar-go-checks/src/test/java/org/sonar/go/checks/TooManyParametersCheckTest.java new file mode 100644 index 00000000..9fc2dd35 --- /dev/null +++ b/sonar-go-checks/src/test/java/org/sonar/go/checks/TooManyParametersCheckTest.java @@ -0,0 +1,35 @@ +/* + * SonarSource Go + * Copyright (C) 2018-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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.go.checks; + +import org.junit.jupiter.api.Test; + +class TooManyParametersCheckTest { + + @Test + void default_threshold() { + SlangVerifier.verify("TooManyParameters.slang", new TooManyParametersCheck()); + } + + @Test + void threshold_3() { + TooManyParametersCheck check = new TooManyParametersCheck(); + check.max = 3; + SlangVerifier.verify("TooManyParameters.threshold.3.slang", check); + } + +} diff --git a/sonar-go-checks/src/test/java/org/sonar/go/checks/VariableAndParameterNameCheckTest.java b/sonar-go-checks/src/test/java/org/sonar/go/checks/VariableAndParameterNameCheckTest.java new file mode 100644 index 00000000..279de541 --- /dev/null +++ b/sonar-go-checks/src/test/java/org/sonar/go/checks/VariableAndParameterNameCheckTest.java @@ -0,0 +1,28 @@ +/* + * SonarSource Go + * Copyright (C) 2018-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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.go.checks; + +import org.junit.jupiter.api.Test; + +class VariableAndParameterNameCheckTest { + + @Test + void test() { + SlangVerifier.verify("VariableAndParameterName.slang", new VariableAndParameterNameCheck()); + } + +} diff --git a/sonar-go-checks/src/test/java/org/sonar/go/checks/WrongAssignmentOperatorCheckTest.java b/sonar-go-checks/src/test/java/org/sonar/go/checks/WrongAssignmentOperatorCheckTest.java new file mode 100644 index 00000000..ba68ecab --- /dev/null +++ b/sonar-go-checks/src/test/java/org/sonar/go/checks/WrongAssignmentOperatorCheckTest.java @@ -0,0 +1,28 @@ +/* + * SonarSource Go + * Copyright (C) 2018-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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.go.checks; + +import org.junit.jupiter.api.Test; + +class WrongAssignmentOperatorCheckTest { + + @Test + void test() { + SlangVerifier.verify("WrongAssignmentOperator.slang", new WrongAssignmentOperatorCheck()); + } + +} diff --git a/sonar-go-checks/src/test/java/org/sonar/go/checks/complexity/CognitiveComplexityTest.java b/sonar-go-checks/src/test/java/org/sonar/go/checks/complexity/CognitiveComplexityTest.java new file mode 100644 index 00000000..b3dace27 --- /dev/null +++ b/sonar-go-checks/src/test/java/org/sonar/go/checks/complexity/CognitiveComplexityTest.java @@ -0,0 +1,139 @@ +/* + * SonarSource Go + * Copyright (C) 2018-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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.go.checks.complexity; + +import org.junit.jupiter.api.Test; +import org.sonarsource.slang.api.Tree; +import org.sonarsource.slang.parser.SLangConverter; + +import static org.assertj.core.api.Assertions.assertThat; + +class CognitiveComplexityTest { + + private SLangConverter parser = new SLangConverter(); + + @Test + void unrelated_statement() { + assertThat(complexity("42;").value()).isZero(); + } + + @Test + void if_statements() { + assertThat(complexity("if (x) { 42 };").value()).isEqualTo(1); + assertThat(complexity("if (x) { 42 } else { 43 };").value()).isEqualTo(2); + assertThat(complexity("if (x) { 42 } else if (y) { 43 };").value()).isEqualTo(2); + assertThat(complexity("if (x) { 42 } else if (y) { 43 } else { 44 };").value()).isEqualTo(3); + } + + @Test + void ternary_operator() { + assertThat(complexity("v = if (x) 42 else 43;").value()).isEqualTo(1); + assertThat(complexity("v = if (x) 42 else if (y) 43 else 44;").value()).isEqualTo(3); + assertThat(complexity("v = if (x) (if (y) 42 else 43) else 44;").value()).isEqualTo(3); + assertThat(complexity("v = if (x) 42 else (if (y) 43 else 44);").value()).isEqualTo(3); + assertThat(complexity("v = if (x) (if (y) 42 else 43) else (if (y) 44 else 45);").value()).isEqualTo(5); + assertThat(complexity("v = if (x) if (y) if (z) 42 else 43 else 44 else 45;").value()).isEqualTo(6); + assertThat(complexity("v = if (x) (if (y) (if (z) 42 else 43) else 44) else 45;").value()).isEqualTo(6); + } + + @Test + void nested_if_statements() { + assertThat(complexity("if (x) { 42 };").value()).isEqualTo(1); + assertThat(complexity("if (x) { 42 } else { 43 };").value()).isEqualTo(2); + assertThat(complexity("if (x) { 42 } else if (y) { 43 };").value()).isEqualTo(2); + assertThat(complexity("if (x) { 42 } else if (y) { if (y) { 43 } else { 44 } };").value()).isEqualTo(5); + assertThat(complexity("if (x) { 42 } else if (y) { 43 } else { 44 };").value()).isEqualTo(3); + assertThat(complexity("if (x) { 42 } else if (y) { 43 } else { if (y) { 44 } else { 45 } };").value()).isEqualTo(6); + + } + + @Test + void loop_statements() { + assertThat(complexity("while (x) { 42 };").value()).isEqualTo(1); + } + + @Test + void match_statements() { + assertThat(complexity("match (x) { else -> 42; };").value()).isEqualTo(1); + assertThat(complexity("match (x) { 'a' -> 0; else -> 42; };").value()).isEqualTo(1); + } + + @Test + void try_catch_statements() { + assertThat(complexity("try { foo; };").value()).isZero(); + assertThat(complexity("try { foo; } catch (e1) { bar; };").value()).isEqualTo(1); + assertThat(complexity("try { foo; } catch (e1) { bar; } catch (e2) { baz; };").value()).isEqualTo(2); + assertThat(complexity("try { foo; } finally { bar; };").value()).isZero(); + } + + @Test + void functions() { + assertThat(complexity("fun foo() { 42 }").value()).isZero(); + assertThat(complexity("fun foo() { f = fun() { 42 }; }").value()).isZero(); + } + + @Test + void binary_operators() { + assertThat(complexity("a == b;").value()).isZero(); + assertThat(complexity("a && b;").value()).isEqualTo(1); + assertThat(complexity("a || b;").value()).isEqualTo(1); + assertThat(complexity("a && b && c;").value()).isEqualTo(1); + assertThat(complexity("a || b || c;").value()).isEqualTo(1); + assertThat(complexity("a || b && c;").value()).isEqualTo(2); + assertThat(complexity("a || b && c || d;").value()).isEqualTo(3); + } + + @Test + void jumps() { + assertThat(complexity("break;").value()).isZero(); + assertThat(complexity("break foo;").value()).isEqualTo(1); + assertThat(complexity("while (x) break;").value()).isEqualTo(1); + assertThat(complexity("while (x) break foo;").value()).isEqualTo(2); + + assertThat(complexity("continue;").value()).isZero(); + assertThat(complexity("continue foo;").value()).isEqualTo(1); + assertThat(complexity("while (x) continue;").value()).isEqualTo(1); + assertThat(complexity("while (x) continue foo;").value()).isEqualTo(2); + } + + @Test + void nesting() { + assertThat(complexity("if (x) a && b;").value()).isEqualTo(2); + assertThat(complexity("if (x) if (y) 42;").value()).isEqualTo(3); + assertThat(complexity("while (x) if (y) 42;").value()).isEqualTo(3); + assertThat(complexity("match (x) { else -> if (y) 42; };").value()).isEqualTo(3); + assertThat(complexity("try { x } catch (e) { if (y) 42; };").value()).isEqualTo(3); + assertThat(complexity("try { if (y) 42; } catch (e) { x };").value()).isEqualTo(2); + assertThat(complexity("fun foo() { if (x) 42; }").value()).isEqualTo(1); + assertThat(complexity("fun foo() { f = fun() { if (x) 42; }; }").value()).isEqualTo(2); + assertThat(complexity("if (x) { f = fun() { if (x) 42; }; };").value()).isEqualTo(4); + } + + @Test + void nesting_with_classes() { + assertThat(complexity("class A { if (x) a && b; }").value()).isEqualTo(2); + assertThat(complexity("class A { fun foo() { if (x) a && b; } }").value()).isEqualTo(2); + assertThat(complexity("class A { fun foo() { fun bar() { if (x) a && b; } } }").value()).isEqualTo(3); + assertThat(complexity("class A { fun foo() { class B { fun bar() { if (x) a && b; } } } }").value()).isEqualTo(2); + assertThat(complexity("class A { fun foo() { if (x) a && b; class B { fun bar() { if (x) a && b; } } } }").value()).isEqualTo(4); + } + + private CognitiveComplexity complexity(String code) { + Tree tree = parser.parse(code); + return new CognitiveComplexity(tree); + } +} diff --git a/sonar-go-checks/src/test/java/org/sonar/go/checks/utils/ExpressionUtilsTest.java b/sonar-go-checks/src/test/java/org/sonar/go/checks/utils/ExpressionUtilsTest.java new file mode 100644 index 00000000..87ab04bb --- /dev/null +++ b/sonar-go-checks/src/test/java/org/sonar/go/checks/utils/ExpressionUtilsTest.java @@ -0,0 +1,109 @@ +/* + * SonarSource Go + * Copyright (C) 2018-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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.go.checks.utils; + +import org.junit.jupiter.api.Test; +import org.sonarsource.slang.api.Tree; +import org.sonarsource.slang.api.UnaryExpressionTree; +import org.sonarsource.slang.impl.BinaryExpressionTreeImpl; +import org.sonarsource.slang.impl.LiteralTreeImpl; +import org.sonarsource.slang.impl.ParenthesizedExpressionTreeImpl; +import org.sonarsource.slang.impl.UnaryExpressionTreeImpl; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.sonar.go.checks.utils.ExpressionUtils.isBinaryOperation; +import static org.sonar.go.checks.utils.ExpressionUtils.isBooleanLiteral; +import static org.sonar.go.checks.utils.ExpressionUtils.isFalseValueLiteral; +import static org.sonar.go.checks.utils.ExpressionUtils.isLogicalBinaryExpression; +import static org.sonar.go.checks.utils.ExpressionUtils.isNegation; +import static org.sonar.go.checks.utils.ExpressionUtils.isTrueValueLiteral; +import static org.sonar.go.checks.utils.ExpressionUtils.skipParentheses; +import static org.sonarsource.slang.api.BinaryExpressionTree.Operator.CONDITIONAL_AND; +import static org.sonarsource.slang.api.BinaryExpressionTree.Operator.CONDITIONAL_OR; +import static org.sonarsource.slang.api.BinaryExpressionTree.Operator.EQUAL_TO; + +class ExpressionUtilsTest { + private static final Tree TRUE_LITERAL = new LiteralTreeImpl(null, "true"); + private static final Tree FALSE_LITERAL = new LiteralTreeImpl(null, "false"); + private static final Tree NUMBER_LITERAL = new LiteralTreeImpl(null, "34"); + private static final Tree TRUE_NEGATED = new UnaryExpressionTreeImpl(null, UnaryExpressionTree.Operator.NEGATE, TRUE_LITERAL); + private static final Tree FALSE_NEGATED = new UnaryExpressionTreeImpl(null, UnaryExpressionTree.Operator.NEGATE, FALSE_LITERAL); + + @Test + void test_boolean_literal() { + assertThat(isBooleanLiteral(TRUE_LITERAL)).isTrue(); + assertThat(isBooleanLiteral(FALSE_LITERAL)).isTrue(); + assertThat(isBooleanLiteral(NUMBER_LITERAL)).isFalse(); + assertThat(isBooleanLiteral(TRUE_NEGATED)).isFalse(); + } + + @Test + void test_false_literal_value() { + assertThat(isFalseValueLiteral(TRUE_LITERAL)).isFalse(); + assertThat(isFalseValueLiteral(FALSE_LITERAL)).isTrue(); + assertThat(isFalseValueLiteral(NUMBER_LITERAL)).isFalse(); + assertThat(isFalseValueLiteral(TRUE_NEGATED)).isTrue(); + assertThat(isFalseValueLiteral(FALSE_NEGATED)).isFalse(); + } + + @Test + void test_true_literal_value() { + assertThat(isTrueValueLiteral(TRUE_LITERAL)).isTrue(); + assertThat(isTrueValueLiteral(FALSE_LITERAL)).isFalse(); + assertThat(isTrueValueLiteral(NUMBER_LITERAL)).isFalse(); + assertThat(isTrueValueLiteral(TRUE_NEGATED)).isFalse(); + assertThat(isTrueValueLiteral(FALSE_NEGATED)).isTrue(); + } + + @Test + void test_negation() { + assertThat(isNegation(FALSE_LITERAL)).isFalse(); + assertThat(isNegation(NUMBER_LITERAL)).isFalse(); + assertThat(isNegation(TRUE_NEGATED)).isTrue(); + } + + @Test + void test_binary_operation() { + Tree binaryAnd = new BinaryExpressionTreeImpl(null, CONDITIONAL_AND, null, TRUE_LITERAL, FALSE_LITERAL); + + assertThat(isBinaryOperation(binaryAnd, CONDITIONAL_AND)).isTrue(); + assertThat(isBinaryOperation(binaryAnd, CONDITIONAL_OR)).isFalse(); + } + + @Test + void test_logical_binary_operation() { + Tree binaryAnd = new BinaryExpressionTreeImpl(null, CONDITIONAL_AND, null, TRUE_LITERAL, FALSE_LITERAL); + Tree binaryOr = new BinaryExpressionTreeImpl(null, CONDITIONAL_OR, null, TRUE_LITERAL, FALSE_LITERAL); + Tree binaryEqual = new BinaryExpressionTreeImpl(null, EQUAL_TO, null, TRUE_LITERAL, FALSE_LITERAL); + + assertThat(isLogicalBinaryExpression(binaryAnd)).isTrue(); + assertThat(isLogicalBinaryExpression(binaryOr)).isTrue(); + assertThat(isLogicalBinaryExpression(binaryEqual)).isFalse(); + assertThat(isLogicalBinaryExpression(TRUE_NEGATED)).isFalse(); + } + + @Test + void test_skip_parentheses() { + Tree parenthesizedExpression1 = new ParenthesizedExpressionTreeImpl(null, TRUE_LITERAL, null, null); + Tree parenthesizedExpression2 = new ParenthesizedExpressionTreeImpl(null, parenthesizedExpression1, null, null); + + assertThat(skipParentheses(parenthesizedExpression1)).isEqualTo(TRUE_LITERAL); + assertThat(skipParentheses(parenthesizedExpression2)).isEqualTo(TRUE_LITERAL); + assertThat(skipParentheses(TRUE_LITERAL)).isEqualTo(TRUE_LITERAL); + } + +} diff --git a/sonar-go-checks/src/test/java/org/sonar/go/checks/utils/FunctionUtilsTest.java b/sonar-go-checks/src/test/java/org/sonar/go/checks/utils/FunctionUtilsTest.java new file mode 100644 index 00000000..c4d67cac --- /dev/null +++ b/sonar-go-checks/src/test/java/org/sonar/go/checks/utils/FunctionUtilsTest.java @@ -0,0 +1,123 @@ +/* + * SonarSource Go + * Copyright (C) 2018-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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.go.checks.utils; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import org.junit.jupiter.api.Test; +import org.sonarsource.slang.api.ASTConverter; +import org.sonarsource.slang.api.FunctionDeclarationTree; +import org.sonarsource.slang.api.FunctionInvocationTree; +import org.sonarsource.slang.api.IdentifierTree; +import org.sonarsource.slang.api.NativeKind; +import org.sonarsource.slang.api.Tree; +import org.sonarsource.slang.api.TreeMetaData; +import org.sonarsource.slang.impl.FunctionInvocationTreeImpl; +import org.sonarsource.slang.impl.IdentifierTreeImpl; +import org.sonarsource.slang.impl.MemberSelectTreeImpl; +import org.sonarsource.slang.impl.NativeTreeImpl; +import org.sonarsource.slang.parser.SLangConverter; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.sonar.go.checks.utils.FunctionUtils.hasFunctionCallFullNameIgnoreCase; +import static org.sonar.go.checks.utils.FunctionUtils.hasFunctionCallNameIgnoreCase; + +class FunctionUtilsTest { + private class TypeNativeKind implements NativeKind { + } + + private static TreeMetaData meta = null; + private static IdentifierTree identifierTree = new IdentifierTreeImpl(meta, "function"); + private static List args = new ArrayList<>(); + + private static final ASTConverter CONVERTER = new SLangConverter(); + + @Test + void test_has_function_name_identifier() { + FunctionInvocationTree tree = new FunctionInvocationTreeImpl(meta, identifierTree, args); + assertThat(hasFunctionCallNameIgnoreCase(tree, "function")).isTrue(); + assertThat(hasFunctionCallNameIgnoreCase(tree, "FuNcTiOn")).isTrue(); + assertThat(hasFunctionCallNameIgnoreCase(tree, "mySuperFunction")).isFalse(); + } + + @Test + void test_has_function_name_method_select() { + Tree member = new IdentifierTreeImpl(meta, "A"); + Tree methodSelect = new MemberSelectTreeImpl(meta, member, identifierTree); + FunctionInvocationTree tree = new FunctionInvocationTreeImpl(meta, methodSelect, args); + assertThat(hasFunctionCallNameIgnoreCase(tree, "function")).isTrue(); + assertThat(hasFunctionCallNameIgnoreCase(tree, "A")).isFalse(); + } + + @Test + void test_has_function_name_unknown() { + Tree nativeNode = new NativeTreeImpl(meta, new TypeNativeKind(), null); + FunctionInvocationTree tree = new FunctionInvocationTreeImpl(meta, nativeNode, args); + assertThat(hasFunctionCallNameIgnoreCase(tree, "function")).isFalse(); + } + + @Test + void test_has_function_full_name_identifier() { + FunctionInvocationTree tree = new FunctionInvocationTreeImpl(meta, identifierTree, args); + assertThat(hasFunctionCallFullNameIgnoreCase(tree, "function")).isTrue(); + assertThat(hasFunctionCallFullNameIgnoreCase(tree, "FuNcTioN")).isTrue(); + assertThat(hasFunctionCallFullNameIgnoreCase(tree, "mySuperFunction")).isFalse(); + assertThat(hasFunctionCallFullNameIgnoreCase(tree)).isFalse(); + } + + @Test + void test_has_function_full_name_method_select() { + IdentifierTree memberA = new IdentifierTreeImpl(meta, "A"); + IdentifierTree memberB = new IdentifierTreeImpl(meta, "B"); + Tree methodSelectAB = new MemberSelectTreeImpl(meta, memberA, memberB); + Tree methodSelect = new MemberSelectTreeImpl(meta, methodSelectAB, identifierTree); + FunctionInvocationTree tree = new FunctionInvocationTreeImpl(meta, methodSelect, args); + + assertThat(hasFunctionCallFullNameIgnoreCase(tree)).isFalse(); + assertThat(hasFunctionCallFullNameIgnoreCase(tree, "function")).isFalse(); + assertThat(hasFunctionCallFullNameIgnoreCase(tree, "A")).isFalse(); + assertThat(hasFunctionCallFullNameIgnoreCase(tree, "B")).isFalse(); + assertThat(hasFunctionCallFullNameIgnoreCase(tree, "A", "B")).isFalse(); + assertThat(hasFunctionCallFullNameIgnoreCase(tree, "A", "function")).isFalse(); + assertThat(hasFunctionCallFullNameIgnoreCase(tree, "B", "function")).isFalse(); + assertThat(hasFunctionCallFullNameIgnoreCase(tree, "A", "B", "function")).isTrue(); + assertThat(hasFunctionCallFullNameIgnoreCase(tree, "A", "B", "function", "C")).isFalse(); + } + + @Test + void test_has_function_full_name_unknown() { + Tree nativeNode = new NativeTreeImpl(meta, new TypeNativeKind(), null); + FunctionInvocationTree tree = new FunctionInvocationTreeImpl(meta, nativeNode, args); + assertThat(hasFunctionCallFullNameIgnoreCase(tree, "function")).isFalse(); + } + + @Test + void test_get_strings_tokens_returns_tokens() { + String code = """ + void fun fooBar() { + val a = "one,two,three"; + foo("one,two$four"); + }"""; + + FunctionDeclarationTree root = (FunctionDeclarationTree) CONVERTER.parse(code, null).children().get(0); + + Set tokens = FunctionUtils.getStringsTokens(root, ",|\\$"); + + assertThat(tokens).containsExactlyInAnyOrder("one", "two", "three", "four"); + } +} diff --git a/sonar-go-checks/src/test/resources/checks/AllBranchesIdentical.slang b/sonar-go-checks/src/test/resources/checks/AllBranchesIdentical.slang new file mode 100644 index 00000000..d2db97ed --- /dev/null +++ b/sonar-go-checks/src/test/resources/checks/AllBranchesIdentical.slang @@ -0,0 +1,26 @@ + + + + if (x) { foo; }; + if (x) { foo; } else { bar; }; + if (x) { foo; } else { foo; }; // Noncompliant +//^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + if (x) { foo; } else if (y) { foo; }; + + if (x) { foo; } else if (y) { foo; } else { bar }; + + if (x) { foo; } else if (y) { foo; } else { foo }; // Noncompliant +//^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + if (x) if (y) return foo else return foo else return bar; // Noncompliant +// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + match (x) { }; + match (x) { 1 -> a; }; + match (x) { 1 -> a; else -> b; }; + match (x) { 1 -> a; else -> a; }; // Noncompliant +//^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + match (x) { 1 -> a; 2 -> a; else -> b; }; + match (x) { 1 -> a; 2 -> a; else -> a; }; // Noncompliant + match (x) { else -> b; }; // Compliant: only default case diff --git a/sonar-go-checks/src/test/resources/checks/BadClassName.slang b/sonar-go-checks/src/test/resources/checks/BadClassName.slang new file mode 100644 index 00000000..f0add39a --- /dev/null +++ b/sonar-go-checks/src/test/resources/checks/BadClassName.slang @@ -0,0 +1,12 @@ +class myClass{} // Noncompliant {{Rename class "myClass" to match the regular expression ^[A-Z][a-zA-Z0-9]*$.}} +// ^^^^^^^ + +class My_Class{} // Noncompliant {{Rename class "My_Class" to match the regular expression ^[A-Z][a-zA-Z0-9]*$.}} + +class my_class{} // Noncompliant {{Rename class "my_class" to match the regular expression ^[A-Z][a-zA-Z0-9]*$.}} + +class MyClass{} // Compliant + +class MyClassC{} // Compliant + +class MyClass${} // Noncompliant {{Rename class "MyClass$" to match the regular expression ^[A-Z][a-zA-Z0-9]*$.}} diff --git a/sonar-go-checks/src/test/resources/checks/BadFunctionName.slang b/sonar-go-checks/src/test/resources/checks/BadFunctionName.slang new file mode 100644 index 00000000..6d5e1d1d --- /dev/null +++ b/sonar-go-checks/src/test/resources/checks/BadFunctionName.slang @@ -0,0 +1,4 @@ +void fun fooBar() {} // OK +fun () {} // OK +void fun foo_bar() {} // Noncompliant {{Rename function "foo_bar" to match the regular expression ^(_|[a-zA-Z0-9]+)$}} +// ^^^^^^^ diff --git a/sonar-go-checks/src/test/resources/checks/BadFunctionName.uppercase.slang b/sonar-go-checks/src/test/resources/checks/BadFunctionName.uppercase.slang new file mode 100644 index 00000000..340e8f96 --- /dev/null +++ b/sonar-go-checks/src/test/resources/checks/BadFunctionName.uppercase.slang @@ -0,0 +1,7 @@ +void fun FOOBAR() {} // OK +void fun foo() {} // Noncompliant {{Rename function "foo" to match the regular expression ^[A-Z]*$}} +// ^^^ + +class A { + fun constructor() {} // Compliant, constructor +} diff --git a/sonar-go-checks/src/test/resources/checks/BooleanInversion.slang b/sonar-go-checks/src/test/resources/checks/BooleanInversion.slang new file mode 100644 index 00000000..a88a16ee --- /dev/null +++ b/sonar-go-checks/src/test/resources/checks/BooleanInversion.slang @@ -0,0 +1,11 @@ +if (!(a == 2)) { }; // Noncompliant {{Use the opposite operator ("!=") instead.}} +// ^^^^^^^^^ +!(i < 10); // Noncompliant {{Use the opposite operator (">=") instead.}} +!(i > 10); // Noncompliant {{Use the opposite operator ("<=") instead.}} +!(i != 10); // Noncompliant {{Use the opposite operator ("==") instead.}} +!(i <= 10); // Noncompliant {{Use the opposite operator (">") instead.}} +!(i >= 10); // Noncompliant {{Use the opposite operator ("<") instead.}} + +if (a != 2) { }; +(i >= 10); +!(a + i); \ No newline at end of file diff --git a/sonar-go-checks/src/test/resources/checks/BooleanLiteral.slang b/sonar-go-checks/src/test/resources/checks/BooleanLiteral.slang new file mode 100644 index 00000000..af86cfa7 --- /dev/null +++ b/sonar-go-checks/src/test/resources/checks/BooleanLiteral.slang @@ -0,0 +1,48 @@ +x == true; // OK - as for now without semantic we do not know if x is nullable or a primitive +x == false; // OK - as for now without semantic we do not know if x is nullable or a primitive +x != true; // OK - as for now without semantic we do not know if x is nullable or a primitive +x != false; // OK - as for now without semantic we do not know if x is nullable or a primitive +true == x; // OK - as for now without semantic we do not know if x is nullable or a primitive +false == x; // OK - as for now without semantic we do not know if x is nullable or a primitive +true != x; // OK - as for now without semantic we do not know if x is nullable or a primitive +false != x; // OK - as for now without semantic we do not know if x is nullable or a primitive +!true; // Noncompliant {{Remove the unnecessary Boolean literal.}} ++true; +!false; // Noncompliant {{Remove the unnecessary Boolean literal.}} +false && foo; // Noncompliant {{Remove the unnecessary Boolean literal.}} +x || true; // Noncompliant {{Remove the unnecessary Boolean literal.}} +x || ((true)); // Noncompliant {{Remove the unnecessary Boolean literal.}} + +!x; // OK +x || foo; // OK +x == y; // OK +z != x; // OK + +x = if (foo) y else false; // Noncompliant {{Remove the unnecessary Boolean literal.}} +x = if (foo) y else true; // Noncompliant {{Remove the unnecessary Boolean literal.}} +x = if (foo) true else y; // Noncompliant {{Remove the unnecessary Boolean literal.}} +x = if (foo) false else y; // Noncompliant {{Remove the unnecessary Boolean literal.}} +x = if (foo) false; // Noncompliant {{Remove the unnecessary Boolean literal.}} + +x = if (foo) x else y; // OK + +x = if (foo) false + else { doSomething(); y }; + +x = if (foo) false + else if (bar) { doSomething(); y } + else true; + +x = if (foo) { doSomething(); y } + else true; + +x = if (a) true // Noncompliant + else false; + +x = if (a) true + else if (b) false + else false; + +x = if (b) + if (a) true // Noncompliant + else false; diff --git a/sonar-go-plugin/src/test/resources/checks/CodeAfterJumpGoCheck.go b/sonar-go-checks/src/test/resources/checks/CodeAfterJumpGoCheck.go similarity index 100% rename from sonar-go-plugin/src/test/resources/checks/CodeAfterJumpGoCheck.go rename to sonar-go-checks/src/test/resources/checks/CodeAfterJumpGoCheck.go diff --git a/sonar-go-checks/src/test/resources/checks/CollapsibleIfStatements.slang b/sonar-go-checks/src/test/resources/checks/CollapsibleIfStatements.slang new file mode 100644 index 00000000..088553e1 --- /dev/null +++ b/sonar-go-checks/src/test/resources/checks/CollapsibleIfStatements.slang @@ -0,0 +1,59 @@ +if (a) { + print(a); + print(b); +}; + +if (a) { +} else if (b) { + if (c) { + } +} else { +}; + +if (a) + if (b) { + } else { + }; + + if (a) { // Noncompliant {{Merge this "if" statement with the nested one.}} +// ^^ + if (b) {} +// ^^< + }; + +if (a) { +} else { + if (c) { // Noncompliant {{Merge this "if" statement with the nested one.}} + if (b) {} + } +}; + +if (a) // Noncompliant {{Merge this "if" statement with the nested one.}} + if (b) + print(); + + +if (a) { // Noncompliant + if (b) { + } +}; + +if (a) { // Noncompliant + if (b) { // Noncompliant + if (c) { + } + } +}; + +if (a) { // Noncompliant + if (b) { + if (c) { + } else { + } + } +}; + +if (a) // Noncompliant + if (b) { + }; + diff --git a/sonar-go-checks/src/test/resources/checks/CommentedCode.slang b/sonar-go-checks/src/test/resources/checks/CommentedCode.slang new file mode 100644 index 00000000..b6739086 --- /dev/null +++ b/sonar-go-checks/src/test/resources/checks/CommentedCode.slang @@ -0,0 +1,14 @@ +// Noncompliant@+2 {{Remove this commented out code.}} +foo(); +// if(a) { print(b); }; +foo(); +// this is a normal comment +bar(); +// Noncompliant@+2 +a = 1; +/* + fun foo() { + print(a); + if (a) return "hello world!" + } +*/ diff --git a/sonar-go-checks/src/test/resources/checks/DuplicateBranch.slang b/sonar-go-checks/src/test/resources/checks/DuplicateBranch.slang new file mode 100644 index 00000000..288d1368 --- /dev/null +++ b/sonar-go-checks/src/test/resources/checks/DuplicateBranch.slang @@ -0,0 +1,97 @@ + if (x) { + foo; + foo; + }; + + if (x) { + foo; + foo; + } else { + bar; + bar; + }; + + if (x) { // handled by S3923 + foo; + foo; + } else { + foo; + foo; + }; + + if (x) { + foo; + foo; + } else if (y) { // Noncompliant {{This branch's code block is the same as the block for the branch on line 22.}} +// ^[el=+4;ec=3] + foo; + foo; + } else { + bar; + bar; + }; + + if (x) { + foo; + foo; + } else if (y) { + bar; + bar; + } else { // Noncompliant + bar; + bar; + }; + + if (x) { + + } else if (y) { + + } else { + bar; bar; + }; + + if (x) { + foo; foo; + } else if (y) { + foo; foo; + } else { + bar; bar; + }; + + if (x) + foo + else if (y) + foo + else + bar + ; + + if (x) + foo + + bar + else if (y) + foo // Noncompliant + + bar + else + bar + ; + + match(x) { + 1 -> + foo + + bar; + 2 -> + foo + + baz; + 3 -> + foo // Noncompliant + + bar; + }; + + match(x) { + 1 -> ; + 2 -> + foo + + baz; + 3 -> ; + }; diff --git a/sonar-go-plugin/src/test/resources/checks/DuplicateBranchGoCheck.go b/sonar-go-checks/src/test/resources/checks/DuplicateBranchGoCheck.go similarity index 98% rename from sonar-go-plugin/src/test/resources/checks/DuplicateBranchGoCheck.go rename to sonar-go-checks/src/test/resources/checks/DuplicateBranchGoCheck.go index 39b0d4b9..e531805f 100644 --- a/sonar-go-plugin/src/test/resources/checks/DuplicateBranchGoCheck.go +++ b/sonar-go-checks/src/test/resources/checks/DuplicateBranchGoCheck.go @@ -1,4 +1,4 @@ -package band +package samples type Header interface { Print() string diff --git a/sonar-go-checks/src/test/resources/checks/DuplicatedFunctionImplementation.slang b/sonar-go-checks/src/test/resources/checks/DuplicatedFunctionImplementation.slang new file mode 100644 index 00000000..cc1b7566 --- /dev/null +++ b/sonar-go-checks/src/test/resources/checks/DuplicatedFunctionImplementation.slang @@ -0,0 +1,101 @@ +void fun foobar() {} +int fun foo_bar() {} // Compliant - has no line + +fun smallF1() { + foo = 1; + bar = foo > 3 || bar +} + +fun smallF2() { // Noncompliant + foo = 1; + bar = foo > 3 || bar +} + +string fun f1() { +// ^^> + foobar = "abc"; + foo = 1; + bar = foo > 3 || bar +} + +int fun f2() { + foobar = "abc"; + foo = 1; + baz = foo > 3 || bar +} + +boolean fun f3() { // Noncompliant {{Update this function so that its implementation is not identical to "f1" on line 14.}} +// ^^ + foobar = "abc"; + foo = 1; + bar = foo > 3 || bar +} + +fun f4() { // Noncompliant {{Update this function so that its implementation is not identical to "f1" on line 14.}} +// ^^ + foobar = "abc"; + foo = 1; + bar = foo > 3 || bar +} + +fun f5(a) { // Compliant - different parameter list + foobar = "abc"; + foo = 1; + bar = foo > 3 || bar +} + +fun f6() { + foo = 1; +} + +fun f7() { // Compliant - only 1 line + foo = 1; +} + +fun f8(int a) { // Compliant + foobar = "abc"; + foo = 1; + bar = foo > 3 || bar +} + +fun f9(int a) { // Noncompliant + foobar = "abc"; foo = 1; bar = foo > 3 || bar +} + +fun f10(string a) { // Compliant - not same parameter type + foobar = "abc"; + foo = 1; + bar = foo > 3 || bar +} + +fun(int a, int b) { // Compliant - not same parameters + foobar = "abc"; + foo = 1; + bar = foo > 3 || bar +} + +fun f11(int a, int b) { + foobar = "abcdefg"; + foo = 1; + bar = foo > 3 || bar +} + +fun f12(int a, int b) { + foobar = "abc"; + foo = 2; + bar = foo > 3 || bar +} + +class A { + + fun constructor(int a) { + foo = 1; + bar = 2; + } + + fun constructor(int a) { // Compliant, constructor + foo = 1; + bar = 2; + } + +} diff --git a/sonar-go-checks/src/test/resources/checks/ElseIfWithoutElse.slang b/sonar-go-checks/src/test/resources/checks/ElseIfWithoutElse.slang new file mode 100644 index 00000000..d644ca39 --- /dev/null +++ b/sonar-go-checks/src/test/resources/checks/ElseIfWithoutElse.slang @@ -0,0 +1,62 @@ +if (x == 0) { + x = 42; +}; + +if (x == 0) { + x = 42; +} else { + x = 43; +}; + +if (x == 0) { + doSomething(); +} else if (x == 1) { // Noncompliant {{Add the missing "else" clause.}} +//^^^^^^^ + doSomethingElse(); +}; + +if (x == 0) { + doSomething(); +} else if (x == 1) { + doSomethingElse(); +} else { + print("Something"); +}; + +if (x == 0) { + doSomething(); +} else if (x == 1) { + doSomethingElse(); +} else if (x == 2) { // Noncompliant {{Add the missing "else" clause.}} + print("Something"); +}; + +if (x == 0) { + break; +} else if (x == 1) { + return; +} else if (x == 3) { + throw(1); +}; + +if (x == 0) { + break; +} else if (x == 1) { // Noncompliant {{Add the missing "else" clause.}} +//^^^^^^^ + doSomething(); +}; + +if (x == 0) { +} else if (x == 1) { // Noncompliant {{Add the missing "else" clause.}} + return; +}; + +if (x >= 0) + if (x > 1) + return 0 + else if (x == 1) + doSomething() + else if (x == 0) { // Noncompliant {{Add the missing "else" clause.}} + doSomethingElse() + }; + diff --git a/sonar-go-checks/src/test/resources/checks/EmptyBlock.slang b/sonar-go-checks/src/test/resources/checks/EmptyBlock.slang new file mode 100644 index 00000000..727d5573 --- /dev/null +++ b/sonar-go-checks/src/test/resources/checks/EmptyBlock.slang @@ -0,0 +1,41 @@ +void fun a() {} + +void fun b() { + // comment +} + +fun () { + // comment +} + +fun () { + // Noncompliant@+1 + if (x > 0) { }; +} + +void fun c(int x) { + // Noncompliant@+1 + if (x > 0) { }; + + if (x > 1) { + // comment + }; + + match (x) { + // comment + }; + + // Noncompliant@+1 + match (x) { + + }; + + match (x) { + // Noncompliant@+1 + 1 -> { }; + 2 -> x; + else -> { x; }; + }; + + while (cond) {} // Compliant - exception to the rule +} \ No newline at end of file diff --git a/sonar-go-checks/src/test/resources/checks/EmptyComment.slang b/sonar-go-checks/src/test/resources/checks/EmptyComment.slang new file mode 100644 index 00000000..bd239b4d --- /dev/null +++ b/sonar-go-checks/src/test/resources/checks/EmptyComment.slang @@ -0,0 +1,15 @@ +/* Some comment */ + +/**/ // Noncompliant + + /* */ // Noncompliant +//^^^^^^ + +// Noncompliant@+1 +/* + +*/ + +// The next line should be compliant +// +// Comment line diff --git a/sonar-go-checks/src/test/resources/checks/EmptyFunction.slang b/sonar-go-checks/src/test/resources/checks/EmptyFunction.slang new file mode 100644 index 00000000..7e47a5c4 --- /dev/null +++ b/sonar-go-checks/src/test/resources/checks/EmptyFunction.slang @@ -0,0 +1,29 @@ +fun noBody(); + +fun notEmpty() { bar() } + +// Noncompliant@+1 +fun empty() {} +// ^^ + +fun containingOnlyAComment() { /* comment */ } + +fun emptyWithEndOfLineComment1() {} // end of line comment + +fun emptyWithEndOfLineComment2() { } /* comment */ + +fun emptyWithEndOfLineCommentOnMultipleLine() { } /* comment +*/ + +fun emptyOnSeveralLine() { +} // comment + +// Noncompliant@+1 +fun empty() { +} + +class A { + // Compliant, constructor + fun constructor() { + } +} diff --git a/sonar-go-checks/src/test/resources/checks/FixMeComment.slang b/sonar-go-checks/src/test/resources/checks/FixMeComment.slang new file mode 100644 index 00000000..717cc162 --- /dev/null +++ b/sonar-go-checks/src/test/resources/checks/FixMeComment.slang @@ -0,0 +1,51 @@ +// comment 1 + +// Noncompliant@+1 +// FIXME + +// Noncompliant@+1 +// FIXME just do it +// ^^^^^ + +// Noncompliant@+1 +// Fixme just do it + +// Noncompliant@+1 +// This is a FIXME just do it + +// This is not aFIXME comment + +/* + Multiline comment +*/ + +// Noncompliant@+2 +/* + FiXmE Multiline comment */ +//^^^^^ + +// Noncompliant@+2 +/* +fixme Multiline comment */ + +// Noncompliant@+1 +//fixme comment +//^^^^^ + +// notafixme comment + +// not2fixme comment + +// a fixmelist + +// Noncompliant@+1 +// fixme: things to do + +// Noncompliant@+1 +// :fixme: things to do + +// Noncompliant@+1 +// valid end of line fixme + +// Noncompliant@+1 +// valid end of file fixme \ No newline at end of file diff --git a/sonar-go-checks/src/test/resources/checks/FunctionCognitiveComplexity.slang b/sonar-go-checks/src/test/resources/checks/FunctionCognitiveComplexity.slang new file mode 100644 index 00000000..7023579f --- /dev/null +++ b/sonar-go-checks/src/test/resources/checks/FunctionCognitiveComplexity.slang @@ -0,0 +1,48 @@ +fun ok() { + if (x) { + if (y) { + foo(); + }; + }; + if (z) { + foo(); + }; +} + +fun ko() { // Noncompliant {{Refactor this method to reduce its Cognitive Complexity from 5 to the 4 allowed.}} [[effortToFix=1]] +// ^^ + if (x) { +//^^< {{+1}} + if (y) { +// ^^< {{+2 (incl 1 for nesting)}} + foo(); + }; + if (z) { +// ^^< {{+2 (incl 1 for nesting)}} + foo(); + }; + }; +} + +fun logical_operators() { // Noncompliant +// ^^^^^^^^^^^^^^^^^ + if (a +//^^< + && b && c +// ^^< + || d || e +// ^^< + && f +// ^^< + || g) { +// ^^< + foo(); + } +} + +fun nesting_anonymous() { // Noncompliant + fun() { + a && b || c && d || e && f; + } +} + diff --git a/sonar-go-checks/src/test/resources/checks/HardcodedCredentials.slang b/sonar-go-checks/src/test/resources/checks/HardcodedCredentials.slang new file mode 100644 index 00000000..432871e3 --- /dev/null +++ b/sonar-go-checks/src/test/resources/checks/HardcodedCredentials.slang @@ -0,0 +1,60 @@ +x = "pass"; +"pass"; +x = "password"; +"password"; +x = "login=a&password="; +"login=a&password="; +"login=a&password= " + value; +"login=a&password=a"; // Noncompliant +x = "login=a&password=xxx"; // Noncompliant {{"password" detected here, make sure this is not a hard-coded credential.}} +// ^^^^^^^^^^^^^^^^^^^^^^ +"login=a&password=xxx"; // Noncompliant +"login=a&passwd=xxx"; // Noncompliant {{"passwd" detected here, make sure this is not a hard-coded credential.}} +"login=a&pwd=xxx"; // Noncompliant {{"pwd" detected here, make sure this is not a hard-coded credential.}} +"login=a&passphrase=xxx"; // Noncompliant {{"passphrase" detected here, make sure this is not a hard-coded credential.}} + variableNameWithPasswordInIt = "xxx"; // Noncompliant {{"Password" detected here, make sure this is not a hard-coded credential.}} +//^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +variableNameWithPasswdInIt = "xxx"; // Noncompliant +variableNameWithPasswdInIt += "xxx"; // Noncompliant +variableNameWithPwdInIt = "xxx"; // Noncompliant {{"Pwd" detected here, make sure this is not a hard-coded credential.}} +A.variableNameWithPwdInIt = "xxx"; // Noncompliant {{"Pwd" detected here, make sure this is not a hard-coded credential.}} +A.B.variableNameWithPwdInIt = "xxx"; // Noncompliant {{"Pwd" detected here, make sure this is not a hard-coded credential.}} +otherVariableNameWithPasswordInIt; +variableNameWithPasswordInIt = native[] { [ "NativeFunctionCall" ] }; +val constValue = "login=a&password=xxx"; // Noncompliant +var passwd = "xxxx"; // Noncompliant +var passphrase = "xxx"; // Noncompliant +var okVariable = "xxxx"; +variableNameWithPasswdInIt = ""; +A.B.variableNameWithPwdInIt = ""; +var passwd = ""; +var passwd = 2; + +// No issue is raised when the matched wordlist item is present in both symbol name and literal string value. +var password = "password"; // Compliant +var myPassword = "users/connection.secretPassword"; // Compliant +myPassword = "users/connection.secretPassword"; // Compliant +var myPassword = "secretPasswd"; // Noncompliant {{"Password" detected here, make sure this is not a hard-coded credential.}} +myPassword = "secretPasswd"; // Noncompliant {{"Password" detected here, make sure this is not a hard-coded credential.}} +var params = "user=admin&password=Password123"; // Noncompliant {{"password" detected here, make sure this is not a hard-coded credential.}} + +// Database queries are compliant +var query = "password=?"; +query = "password=:password"; +query = "password=:param"; +query = "password='" + password + "'"; +query = "password=%s"; +query = "password=%v"; + +// String format is compliant +query = "password={0}"; + +// Support URI +var uri = "http://user:azer:ty123@domain.com"; // Noncompliant +var uri = "https://:azerty123@domain.com/path"; // Noncompliant {{Review this hard-coded URL, which may contain a credential.}} +var uri = "http://anonymous:anonymous@domain.com"; // Compliant, user and password are the same +var uri = "http://user:@domain.com"; +var uri = "http://user@domain.com:80"; +var uri = "http://domain.com/user:azerty123"; +var uri = "too-long-url-scheme://user:123456@server.com"; +var uri = "https:// invalid::url::format"; diff --git a/sonar-go-checks/src/test/resources/checks/HardcodedIp.slang b/sonar-go-checks/src/test/resources/checks/HardcodedIp.slang new file mode 100644 index 00000000..3c19146d --- /dev/null +++ b/sonar-go-checks/src/test/resources/checks/HardcodedIp.slang @@ -0,0 +1,86 @@ +x = 120; +"120"; +ip = "1.2.3.4"; // Noncompliant {{Make sure using this hardcoded IP address is safe here.}} +// ^^^^^^^^^ +"1.2.3.4"; // Noncompliant +"1.2.3.4:80"; // Noncompliant +"1.2.3.4:8080"; // Noncompliant +"1.2.3.4:a"; +"1.2.3.4.5"; + +// Noncompliant@+1 {{Make sure using this hardcoded IP address is safe here.}} +url = "http://192.168.0.1/admin.html"; +// Noncompliant@+1 +url = "http://192.168.0.1:8181/admin.html"; +url2 = "http://www.example.org"; + +notAnIp1 = "0.0.0.1234"; +notAnIp2 = "1234.0.0.0"; +notAnIp3 = "1234.0.0.0.0.1234"; +notAnIp4 = ".0.0.0.0"; +notAnIp5 = "0.256.0.0"; + +ip = "0.00.0.0"; // Compliant +ip = "1.2.03.4"; // Compliant + +fileName = "v0.0.1.200__do_something.sql"; // Compliant - suffixed and prefixed +version = "1.0.0.0-1"; // Compliant - suffixed + +"1080:0:0:0:8:800:200C:417A"; // Noncompliant {{Make sure using this hardcoded IP address is safe here.}} +"[1080::8:800:200C:417A]"; // Noncompliant +"::800:200C:417A"; // Noncompliant +"1080:800:200C::"; // Noncompliant +"::FFFF:129.144.52.38"; // Noncompliant +"::129.144.52.38"; // Noncompliant +"::FFFF:38"; // Noncompliant +"::100"; // Noncompliant +"1080:0:0:0:8:200C:131.107.129.8"; // Noncompliant +"1080:0:0::8:200C:131.107.129.8"; // Noncompliant + +"1080:0:0:0:8:800:200C:417G"; // Compliant - not valid IPv6 +"1080:0:0:0:8::800:200C:417A"; // Compliant - not valid IPv6 +"1080:0:0:0:8:::200C:417A"; // Compliant - not valid IPv6 +"1080:0:0:0:8"; // Compliant - not valid IPv6 +"1080:0::0:0:8::200C:417A"; // Compliant - not valid IPv6 +"1080:0:0:0:8::200C:417A:"; // Compliant - not valid IPv6 +"1080:0:0:0:8::200C:131.107.129.8"; // Compliant - not valid IPv6 +"1080:0:0:0:8::200C:256.256.129.8"; // Compliant - not valid IPv6 +"1080:0:0:0:8:200C:200C:131.107.129.8"; // Compliant - not valid IPv6 +"1080:0:0:0:8:131.107.129.8"; // Compliant - not valid IPv6 +"1080:0::0::8:200C:131.107.129.8"; // Compliant - not valid IPv6 +"1080:0:0:0:8:200C:131.107.129"; // Compliant - not valid IPv6 +"1080:0:0:0:8:200C:417A:131.107"; // Compliant - not valid IPv6 + +// Noncompliant@+1 {{Make sure using this hardcoded IP address is safe here.}} +"http://[2002:db8:1f70::999:de8:7648:6e8]"; +// Noncompliant@+1 +"http://[2002:db8:1f70::999:de8:7648:6e8]:100/"; +// Noncompliant@+1 + "https://[3FFE:1A05:510:1111:0:5EFE:131.107.129.8]:8080/"; +//^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +// Noncompliant@+1 +"https://[3FFE::1111:0:5EFE:131.107.129.8]:8080/"; + +"ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"; // Noncompliant + +// Exceptions +"0.0.0.0"; +"::1"; +"000:00::1"; +"255.255.255.255"; +"255.255.255.255:80"; +"2.5.255.255"; +"127.5.255.255"; +"http://[::0]:100/"; +"0000:0000:0000:0000:0000:0000:0000:0000"; +"192.0.2.0"; +"198.51.100.0"; +"203.0.113.0"; +"2001:db8:3:4:5:6:7:8"; +"::ffff:0:127.0.0.1"; +"::ffff:0:127.100.150.200"; +"::ffff:0:127.255.255.255"; +"::ffff:127.0.0.1"; +"::ffff:127.100.150.200"; +"::ffff:127.255.255.255"; diff --git a/sonar-go-checks/src/test/resources/checks/IdenticalBinaryOperand.slang b/sonar-go-checks/src/test/resources/checks/IdenticalBinaryOperand.slang new file mode 100644 index 00000000..28805d7e --- /dev/null +++ b/sonar-go-checks/src/test/resources/checks/IdenticalBinaryOperand.slang @@ -0,0 +1,21 @@ + x == 1; + 1 == 1; // Noncompliant {{Correct one of the identical sub-expressions on both sides this operator}} + 1 == (1); // Noncompliant {{Correct one of the identical sub-expressions on both sides this operator}} + (1 + 2) == 1 + 2; // Noncompliant + (1 + 2) == ((1 + 2)); // Noncompliant + (1 + 2) == ((1 + 2 + 3)); + x +//^> + == x; // Noncompliant +// ^ + 1 == 2; + x = x; + x + x; + x * x; + x <= x; // Noncompliant + _x <= _x; // Noncompliant + x_ <= x_; // Noncompliant + _x <= x_; + x <= _; + _ <= y; + _ <= _; diff --git a/sonar-go-checks/src/test/resources/checks/IdenticalConditions.slang b/sonar-go-checks/src/test/resources/checks/IdenticalConditions.slang new file mode 100644 index 00000000..ee2ee132 --- /dev/null +++ b/sonar-go-checks/src/test/resources/checks/IdenticalConditions.slang @@ -0,0 +1,26 @@ +// if + +if (x) { +} else if (x) { // Noncompliant {{This condition duplicates the one on line 3.}} +}; + +if (x) {}; +if (x) {} else {}; +if (x) {} else if (y) {}; +if (x) {} else if (x) {}; // Noncompliant +// ^> ^ +if (x) {} else if (y) {} else if (z) {}; +if (x) {} else if (y) {} else if (y) {}; // Noncompliant +// ^> ^ +if (x) {} else if (x) {} else if (x) {}; // Noncompliant 2 +if (x) {} else if (y) {} else if ((y)) {}; // Noncompliant + + +// match + +match (x) { 1 -> a; }; +match (x) { 1 -> a; else -> b; }; +match (x) { 1 -> a; 2 -> b; }; +match (x) { 1 -> a; 1 -> b; }; // Noncompliant +// ^> ^ +match (x) { 1 -> a; (1) -> b; }; // Noncompliant diff --git a/sonar-go-checks/src/test/resources/checks/IfConditionalAlwaysTrueOrFalse.slang b/sonar-go-checks/src/test/resources/checks/IfConditionalAlwaysTrueOrFalse.slang new file mode 100644 index 00000000..5c6c9479 --- /dev/null +++ b/sonar-go-checks/src/test/resources/checks/IfConditionalAlwaysTrueOrFalse.slang @@ -0,0 +1,38 @@ +if (true) { // Noncompliant {{Remove this useless "if" statement.}} +// ^^^^ + return 1 +}; + +if (false) { // Noncompliant + return 1 +}; + +if (condition) { + return 1 +} else if (true) { // Noncompliant + return 1 +}; + +if (true) // Noncompliant + return 1; + +if (((true))) // Noncompliant + return 1; + +if (!true) // Noncompliant + return 1 +else if (cond && false) { // Noncompliant + return 1 +} else if (cond || false) + return 1 +else if (cond1 || cond2 || true) // Noncompliant + return 1 +else if (cond && cond2 && !true && cond3) { // Noncompliant + return 1 +} else if (cond && !(cond2 && (!true && cond3))) { // Noncompliant + return 1 +}; + + +x = if (true) 1 else 2; // Noncompliant +x = if (!condition) 1 else 2; \ No newline at end of file diff --git a/sonar-go-checks/src/test/resources/checks/MatchCaseTooBig_3.slang b/sonar-go-checks/src/test/resources/checks/MatchCaseTooBig_3.slang new file mode 100644 index 00000000..f91b21e7 --- /dev/null +++ b/sonar-go-checks/src/test/resources/checks/MatchCaseTooBig_3.slang @@ -0,0 +1,25 @@ +match (x) { + 1 -> { // Noncompliant {{Reduce this case clause number of lines from 4 to at most 3, for example by extracting code into methods.}} +// ^^^^ + a = 1; + print(a); + }; + else -> b; +}; + +match (x) { + 1 -> foo(); + else -> { // Noncompliant {{Reduce this case clause number of lines from 4 to at most 3, for example by extracting code into methods.}} +// ^^^^^^^ + a = 1; + print(a); + }; +}; + +match (x) { + 1 -> foo(); // OK + // comments should be ignored + // another comment + + else -> b; +}; diff --git a/sonar-go-checks/src/test/resources/checks/MatchCaseTooBig_5.slang b/sonar-go-checks/src/test/resources/checks/MatchCaseTooBig_5.slang new file mode 100644 index 00000000..3ec0825a --- /dev/null +++ b/sonar-go-checks/src/test/resources/checks/MatchCaseTooBig_5.slang @@ -0,0 +1,17 @@ +match (x) { + expression -> { // Noncompliant {{Reduce this case clause number of lines from 8 to at most 5, for example by extracting code into methods.}} + // ^^^^^^^^^^^^^ + a = 1; + foo(); + bar(); + if (a == 1) { + print(1); + }; + }; + else -> b; +}; + +match (x) { + 1 -> foo(); // OK + else -> b; +}; diff --git a/sonar-go-checks/src/test/resources/checks/MatchWithoutElse.slang b/sonar-go-checks/src/test/resources/checks/MatchWithoutElse.slang new file mode 100644 index 00000000..b5942513 --- /dev/null +++ b/sonar-go-checks/src/test/resources/checks/MatchWithoutElse.slang @@ -0,0 +1,41 @@ + match (x) { }; // Noncompliant {{Add a default clause to this "match" statement.}} +//^^^^^ + match (x) { 1 -> 1; }; // Noncompliant {{Add a default clause to this "match" statement.}} + match (x) { 1 -> 1; else -> 2; }; + match (x) { else -> 2; 1 -> 1; }; + + int fun foo() { + match (x) { 1 -> 1; }; // Noncompliant + match (x) { 1 -> 1; else -> 2; }; + + int val value = match (x) { 1 -> 1; }; // Noncompliant + value = if (y) { match (x) { 1 -> 1; }; } else 2; // Noncompliant + + while (y) { + match (x) { else -> 2; }; + match (x) { 1 -> 1; }; // Noncompliant + }; + + native[] { [ + match (x) { // Noncompliant + 1 -> { match (y) { // Noncompliant + 1 -> 1; + 2 -> { + var c; + match (z) { // Noncompliant + 1 -> { c = 2; }; + }; + }; + 3 -> { + var variable; + match (c) { + 1 -> variable = 1; + else -> variable = 2; + }; + variable; + }; + }; + }; + } + ] } + } diff --git a/sonar-go-checks/src/test/resources/checks/NestedMatch.slang b/sonar-go-checks/src/test/resources/checks/NestedMatch.slang new file mode 100644 index 00000000..034b0f6b --- /dev/null +++ b/sonar-go-checks/src/test/resources/checks/NestedMatch.slang @@ -0,0 +1,47 @@ +match (x) { 1 -> a; else -> b; }; // OK + +match (x) { + 1 -> a; + 2 -> match (y) { // Noncompliant {{Refactor the code to eliminate this nested "match".}} + // ^^^^^ + 3 -> c; + else -> d; + }; + else -> b; +}; + +match (x) { + 1 -> a; + 2 -> { + match (y) { // Noncompliant {{Refactor the code to eliminate this nested "match".}} + 3 -> c; + else -> d; + }; + match (z) { // Noncompliant {{Refactor the code to eliminate this nested "match".}} + 3 -> c; + else -> d; + }; + }; + else -> b; +}; + +match (x) { + 1 -> a; + 2 -> match (y) { // Noncompliant {{Refactor the code to eliminate this nested "match".}} + 3 -> c; + else -> match (z) { // Noncompliant {{Refactor the code to eliminate this nested "match".}} + 4 -> d; + else -> e; + }; + }; + else -> b; +}; + +match (x) { + 1 -> a; + 2 -> b; + else -> match (y) { // Noncompliant {{Refactor the code to eliminate this nested "match".}} + 3 -> c; + else -> d; + }; +}; \ No newline at end of file diff --git a/sonar-go-checks/src/test/resources/checks/OctalValues.slang b/sonar-go-checks/src/test/resources/checks/OctalValues.slang new file mode 100644 index 00000000..9feade64 --- /dev/null +++ b/sonar-go-checks/src/test/resources/checks/OctalValues.slang @@ -0,0 +1,17 @@ +17000; + + 02522; // Noncompliant {{Use decimal values instead of octal ones.}} +// ^^^^^ +0o2522; // Noncompliant +0O2522; // Noncompliant + +"0o2522"; +"02522"; +"0O2522"; + +a + 022; // Noncompliant +// ^^^ + +02; // Compliant - part of exceptions +0077; // Compliant - part of exceptions +0o077; // Compliant - part of exceptions \ No newline at end of file diff --git a/sonar-go-plugin/src/test/resources/checks/OneStatementPerLineGoCheck.go b/sonar-go-checks/src/test/resources/checks/OneStatementPerLineGoCheck.go similarity index 100% rename from sonar-go-plugin/src/test/resources/checks/OneStatementPerLineGoCheck.go rename to sonar-go-checks/src/test/resources/checks/OneStatementPerLineGoCheck.go diff --git a/sonar-go-checks/src/test/resources/checks/RedundantParentheses.slang b/sonar-go-checks/src/test/resources/checks/RedundantParentheses.slang new file mode 100644 index 00000000..c15c4aa8 --- /dev/null +++ b/sonar-go-checks/src/test/resources/checks/RedundantParentheses.slang @@ -0,0 +1,15 @@ +x = x + 1; +x = (x + 1); +x = ((x + 1)); // Noncompliant +// ^ ^< +x = (((x + 1))); // Noncompliant 2 +x = (x + 1) * (x + 2); +x = x + (1 * x) + 2; +x = x + ((1 * x)) + 2; // Noncompliant +x = (1); +x = ((1)); // Noncompliant +// ^ ^< + +if (x && (x + 1 > 0)) {}; +if (x && ((x + 1 > 0))) {}; // Noncompliant +if (x && ((x + 1) > 0)) {}; diff --git a/sonar-go-checks/src/test/resources/checks/SelfAssignment.slang b/sonar-go-checks/src/test/resources/checks/SelfAssignment.slang new file mode 100644 index 00000000..f107114a --- /dev/null +++ b/sonar-go-checks/src/test/resources/checks/SelfAssignment.slang @@ -0,0 +1,14 @@ + x = 1; + x = x + 1; + x += x; + x = x; // Noncompliant {{Remove or correct this useless self-assignment.}} +//^^^^^ + native[] { [x] } = x; // Ex: this.x = x + + // Ex: this.x = this.x + // Noncompliant@+1 + native[] { [x] } = native[] { + [ + x; + ] + }; \ No newline at end of file diff --git a/sonar-go-checks/src/test/resources/checks/StringLiteralDuplicated.slang b/sonar-go-checks/src/test/resources/checks/StringLiteralDuplicated.slang new file mode 100644 index 00000000..060cf802 --- /dev/null +++ b/sonar-go-checks/src/test/resources/checks/StringLiteralDuplicated.slang @@ -0,0 +1,34 @@ +x = "string literal1"; // Noncompliant {{Define a constant instead of duplicating this literal "string literal1" 3 times.}} [[effortToFix=2]] +// ^^^^^^^^^^^^^^^^^ +x += "string literal1" + "other string literal"; +// <^^^^^^^^^^^^^^^^^ +native[] { [] } = native[] { + [ + "string literal1" +// <^^^^^^^^^^^^^^^^^ + ] +}; + +void fun function1(string abcde) { + v = "string literal2" + "string literal2" // Compliant - literal only appears twice +} +"string literal3"; "string literal3"; +"string literal3${x}"; // Compliant - string entries of string templates not considered as string literals + +void fun funtcion2(int abcde) { + if (abcde == "string literal4") { // Noncompliant {{Define a constant instead of duplicating this literal "string literal4" 5 times.}} [[effortToFix=4]] +// ^^^^^^^^^^^^^^^^^ + } +} + +match("string literal4") { +// <^^^^^^^^^^^^^^^^^ + 1 -> "string literal4"; +// <^^^^^^^^^^^^^^^^^ + "string literal4" -> "string literal4"; +// <^^^^^^^^^^^^^^^^^ <^^^^^^^^^^^^^^^^^ +}; + +"abcd"; "abcd"; "abcd"; "abcd"; // Compliant - string length smaller than threshold +"string_literal5"; "string_literal5"; +"string_literal5"; "string_literal5"; // Compliant - single word \ No newline at end of file diff --git a/sonar-go-checks/src/test/resources/checks/StringLiteralDuplicated.threshold_4.slang b/sonar-go-checks/src/test/resources/checks/StringLiteralDuplicated.threshold_4.slang new file mode 100644 index 00000000..137c7828 --- /dev/null +++ b/sonar-go-checks/src/test/resources/checks/StringLiteralDuplicated.threshold_4.slang @@ -0,0 +1,5 @@ + "string literal1"; "string literal1"; "string literal1"; // Compliant - only appears 3 times which is less than the custom threshold of 4 + "string literal2"; // Noncompliant {{Define a constant instead of duplicating this literal "string literal2" 4 times.}} +//^^^^^^^^^^^^^^^^^ + "string literal2"; "string literal2"; "string literal2"; +//<^^^^^^^^^^^^^^^^^ <^^^^^^^^^^^^^^^^^ <^^^^^^^^^^^^^^^^^ diff --git a/sonar-go-checks/src/test/resources/checks/Tabs_compliant.slang b/sonar-go-checks/src/test/resources/checks/Tabs_compliant.slang new file mode 100644 index 00000000..12b26e56 --- /dev/null +++ b/sonar-go-checks/src/test/resources/checks/Tabs_compliant.slang @@ -0,0 +1,3 @@ +a = 1; +b = 4; + x = 1; \ No newline at end of file diff --git a/sonar-go-checks/src/test/resources/checks/Tabs_noncompliant.slang b/sonar-go-checks/src/test/resources/checks/Tabs_noncompliant.slang new file mode 100644 index 00000000..bf74e261 --- /dev/null +++ b/sonar-go-checks/src/test/resources/checks/Tabs_noncompliant.slang @@ -0,0 +1,3 @@ +// Noncompliant@0 {{Replace all tab characters in this file "Tabs_noncompliant.slang" by sequences of white-spaces.}} + a = 1; + tabOnTheLeft = 1; diff --git a/sonar-go-checks/src/test/resources/checks/TodoComment.slang b/sonar-go-checks/src/test/resources/checks/TodoComment.slang new file mode 100644 index 00000000..f1b0565e --- /dev/null +++ b/sonar-go-checks/src/test/resources/checks/TodoComment.slang @@ -0,0 +1,51 @@ +// comment 1 + +// Noncompliant@+1 +// TODO + +// Noncompliant@+1 +// TODO just do it +// ^^^^ + +// Noncompliant@+1 +// Todo just do it + +// Noncompliant@+1 +// This is a TODO just do it + +// This is not aTODO comment + +/* + Multiline comment +*/ + +// Noncompliant@+2 +/* + TODO Multiline comment */ +//^^^^ + +// Noncompliant@+2 +/* +TODO Multiline comment */ + +// Noncompliant@+1 +//todo comment +//^^^^ + +// notatodo comment + +// not2todo comment + +// a todolist + +// Noncompliant@+1 +// todo: things to do + +// Noncompliant@+1 +// :TODO: things to do + +// Noncompliant@+1 +// valid end of line todo + +// Noncompliant@+1 +// valid end of file todo \ No newline at end of file diff --git a/sonar-go-checks/src/test/resources/checks/TooComplexExpression_2.slang b/sonar-go-checks/src/test/resources/checks/TooComplexExpression_2.slang new file mode 100644 index 00000000..dc90567f --- /dev/null +++ b/sonar-go-checks/src/test/resources/checks/TooComplexExpression_2.slang @@ -0,0 +1,3 @@ + a && b; + a && b || c; + a && b || c && d; // Noncompliant {{Reduce the number of conditional operators (3) used in the expression (maximum allowed 2).}} diff --git a/sonar-go-checks/src/test/resources/checks/TooComplexExpression_3.slang b/sonar-go-checks/src/test/resources/checks/TooComplexExpression_3.slang new file mode 100644 index 00000000..6f38b096 --- /dev/null +++ b/sonar-go-checks/src/test/resources/checks/TooComplexExpression_3.slang @@ -0,0 +1,12 @@ + a && b; + a && b || c; + a && b || c && d; + a && b || c && d || e; // Noncompliant {{Reduce the number of conditional operators (4) used in the expression (maximum allowed 3).}} [[effortToFix=1]] + a && b || c && d || e && f; // Noncompliant [[effortToFix=2]] +//^^^^^^^^^^^^^^^^^^^^^^^^^^ +if (a && b || c && d || e && f) {}; // Noncompliant +if (a && (b || c) && (d || e && f)) {}; // Noncompliant +if (a && b || c) {}; +if (!(a && b || c && d)) {}; +if (!(a && b || c && d || e)) {}; // Noncompliant +foo(a && b) && foo(a || b) && foo(a && b); diff --git a/sonar-go-checks/src/test/resources/checks/TooDeeplyNestedStatements.max_2.slang b/sonar-go-checks/src/test/resources/checks/TooDeeplyNestedStatements.max_2.slang new file mode 100644 index 00000000..62d8b012 --- /dev/null +++ b/sonar-go-checks/src/test/resources/checks/TooDeeplyNestedStatements.max_2.slang @@ -0,0 +1,13 @@ + if (true) {} +//^^> + else if (true) {} + else if (true) { + if (true) { // Compliant +// ^^> + if (true) {// Noncompliant {{Refactor this code to not nest more than 2 control flow statements.}} +// ^^ + if (true) { + }; + }; + }; + }; \ No newline at end of file diff --git a/sonar-go-checks/src/test/resources/checks/TooDeeplyNestedStatements.slang b/sonar-go-checks/src/test/resources/checks/TooDeeplyNestedStatements.slang new file mode 100644 index 00000000..4606206d --- /dev/null +++ b/sonar-go-checks/src/test/resources/checks/TooDeeplyNestedStatements.slang @@ -0,0 +1,125 @@ +if (false) { // Compliant +}; + + if (true) {} +//^^> + else if (true) {} + else if (true) { + if (true) { // Compliant +// ^^> + if (true) { +// ^^> + if (true) { +// ^^> + if (true) {} // Noncompliant {{Refactor this code to not nest more than 4 control flow statements.}} +// ^^ + }; + }; + }; + }; + +if (false) { // Compliant + if (true) { // Compliant + } else { + if (false) { // Compliant + if (true) { // Compliant + if (false) { // Noncompliant {{Refactor this code to not nest more than 4 control flow statements.}} + if (false) { // Compliant + }; + } else if (true) { // Compliant + } else { + if (false) { // Compliant + }; + }; + }; + }; + }; +}; + +if (false) { // Compliant +} else if (false) { // Compliant +} else if (false) { // Compliant +} else if (false) { // Compliant +} else if (false) { // Compliant +}; + +if (false) // Compliant + if (false) // Compliant + if (false) // Compliant + if (false) // Compliant + if (true) // Noncompliant + println(); + + for (int var x = list) { // Compliant +//^^^> + for (int var o = objects) { // Compliant +// ^^^> + while (false) { // Compliant +// ^^^^^> + while (false) { // Compliant +// ^^^^^> + for (int var p = list) { // Noncompliant + }; + + while (false){ // Noncompliant + }; + + do { } while (false); // Noncompliant + + if (false) { // Noncompliant + }; + + match (p) { // Noncompliant +// ^^^^^ + }; + + try { // Noncompliant + + } catch () { }; + + x = if (false) 1 else 2; + x = if (false) { // Noncompliant + foo(); + 1 + } else 2; + }; + }; + + x = if (condition) if (x == 0) 1 else 2 else 3; + x = if (condition) (if (x == 0) 1 else 2) else 3; + if (if (x == 0) a() else b()) { + }; + + while (false) { + if (condition) if (x == 0) a() else b(); // Noncompliant + + for (int var o = objects) if (x == 0) 1 else 2; // Noncompliant + + for (int var x = if (x == 0) 1 else 2) { + }; + + while (if (x == 0) false else true) { + }; + + match (if (x == 0) 1 else 2) { + 1 -> if (x == 0) 1 else 2; // Noncompliant + else -> if (x == 0) 1 else 2; // Noncompliant + }; + }; + + }; +}; + + +while (true) { + if (true) { + } else if (true) { + } else { + if (true) { + if (true) { + if (true) { // Noncompliant + }; + }; + }; + }; +}; diff --git a/sonar-go-checks/src/test/resources/checks/TooLongFunction_3.slang b/sonar-go-checks/src/test/resources/checks/TooLongFunction_3.slang new file mode 100644 index 00000000..6c0f920f --- /dev/null +++ b/sonar-go-checks/src/test/resources/checks/TooLongFunction_3.slang @@ -0,0 +1,48 @@ +fun f1() { + a; a; +} + +fun f1() { // Noncompliant {{This function has 4 lines of code, which is greater than the 3 authorized. Split it into smaller functions.}} +// ^^ + a; + a; +} + +fun f1() { + + a; +} + +fun f1() { + // comment + a; +} + +int fun foo( // Compliant, no line of code +p1, +p2, +p3, +p4) +{ +} + +int fun foo( +p1, +p2, +p3, +p4); + +int fun ( +p1, +p2, +p3, +p4); + + int fun () // Noncompliant +// ^^^^^^^^^^ +{ + a; + a; + a; + a; +} diff --git a/sonar-go-checks/src/test/resources/checks/TooLongFunction_4.slang b/sonar-go-checks/src/test/resources/checks/TooLongFunction_4.slang new file mode 100644 index 00000000..fcd37c5d --- /dev/null +++ b/sonar-go-checks/src/test/resources/checks/TooLongFunction_4.slang @@ -0,0 +1,11 @@ +fun f1() { // Noncompliant {{This function has 5 lines of code, which is greater than the 4 authorized. Split it into smaller functions.}} + a; + a; + a; +} + +fun f1() { + a; + + a; +} diff --git a/sonar-go-checks/src/test/resources/checks/TooLongLine_120.slang b/sonar-go-checks/src/test/resources/checks/TooLongLine_120.slang new file mode 100644 index 00000000..aee01226 --- /dev/null +++ b/sonar-go-checks/src/test/resources/checks/TooLongLine_120.slang @@ -0,0 +1,9 @@ +int fun (p1, p2) {} + +// short comment + +// Noncompliant@+1 {{Split this 124 characters long line (which is greater than 120 authorized).}} +int fun fooVeryLongName(fooVeryLongName1, fooVeryLongName2, fooVeryLongName3, fooVeryLongName4, fooVeryLongName5, p6, p7) {} + +// Noncompliant@+1 {{Split this 125 characters long line (which is greater than 120 authorized).}} +// this is a very comment that should raise an issue when its length is greater that 120 characters that is the default value \ No newline at end of file diff --git a/sonar-go-checks/src/test/resources/checks/TooLongLine_40.slang b/sonar-go-checks/src/test/resources/checks/TooLongLine_40.slang new file mode 100644 index 00000000..4cae4bb7 --- /dev/null +++ b/sonar-go-checks/src/test/resources/checks/TooLongLine_40.slang @@ -0,0 +1,4 @@ +int fun (p1, p2, p3) {} + +// Noncompliant@+1 +int fun funWithLongName(p1, p2, p3) { println(10);} \ No newline at end of file diff --git a/sonar-go-checks/src/test/resources/checks/TooManyCases_3.slang b/sonar-go-checks/src/test/resources/checks/TooManyCases_3.slang new file mode 100644 index 00000000..0ed4227f --- /dev/null +++ b/sonar-go-checks/src/test/resources/checks/TooManyCases_3.slang @@ -0,0 +1,30 @@ + match (x) { + 1 -> 'a'; + 2 -> 'b'; + 3 -> 'c'; + }; + + match (x) { // Noncompliant {{Reduce the number of match branches from 4 to at most 3.}} + 1 -> 'a'; + 2 -> 'b'; + 3 -> 'c'; + 4 -> 'd'; + }; + + match (x) { + 1 -> 'a'; + 2 -> 'b'; + else -> 'c'; + }; + + match (x) { // Noncompliant +//^^^^^ + 1 -> 'a'; +// ^^^^< + 2 -> 'b'; +// ^^^^< + 3 -> 'c'; +// ^^^^< + else -> 'd'; +// ^^^^^^^< +}; diff --git a/sonar-go-checks/src/test/resources/checks/TooManyCases_4.slang b/sonar-go-checks/src/test/resources/checks/TooManyCases_4.slang new file mode 100644 index 00000000..f224cf75 --- /dev/null +++ b/sonar-go-checks/src/test/resources/checks/TooManyCases_4.slang @@ -0,0 +1,14 @@ +match (x) { + 1 -> 'a'; + 2 -> 'b'; + 3 -> 'c'; + 4 -> 'd'; +}; + +match (x) { // Noncompliant + 1 -> 'a'; + 2 -> 'b'; + 3 -> 'c'; + 4 -> 'd'; + 5 -> 'e'; +}; diff --git a/sonar-go-checks/src/test/resources/checks/TooManyLinesOfCodeFile.max_4.slang b/sonar-go-checks/src/test/resources/checks/TooManyLinesOfCodeFile.max_4.slang new file mode 100644 index 00000000..f413e7b7 --- /dev/null +++ b/sonar-go-checks/src/test/resources/checks/TooManyLinesOfCodeFile.max_4.slang @@ -0,0 +1,15 @@ +// Noncompliant@0 {{File "TooManyLinesOfCodeFile.max_4.slang" has 5 lines, which is greater than 4 authorized. Split it into smaller files.}} +// comment - not a line of code + +if (cond) { + a = b + 1 +}; + + +x = 4; + +x; + +/* + * There are 5 lines of code in this file + */ diff --git a/sonar-go-checks/src/test/resources/checks/TooManyLinesOfCodeFile.max_5.slang b/sonar-go-checks/src/test/resources/checks/TooManyLinesOfCodeFile.max_5.slang new file mode 100644 index 00000000..164b830f --- /dev/null +++ b/sonar-go-checks/src/test/resources/checks/TooManyLinesOfCodeFile.max_5.slang @@ -0,0 +1,14 @@ +// comment - not a line of code + +if (cond) { + a = b + 1 +}; + + +x = 4; + +x; + +/* + * There are 5 lines of code in this file + */ diff --git a/sonar-go-checks/src/test/resources/checks/TooManyParameters.slang b/sonar-go-checks/src/test/resources/checks/TooManyParameters.slang new file mode 100644 index 00000000..dc83e001 --- /dev/null +++ b/sonar-go-checks/src/test/resources/checks/TooManyParameters.slang @@ -0,0 +1,17 @@ +int fun foo(p1, p2, p3, p4, p5, p6, p7) {} +int fun (p1, p2, p3, p4, p5, p6, p7) {} + +int fun (p1, p2, p3, p4, p5, p6, p7, p8) {} // Noncompliant +int fun foo(p1, p2, p3, p4, p5, p6, p7, p8) {} // Noncompliant {{This function has 8 parameters, which is greater than the 7 authorized.}} +// ^^^ ^^< + +int fun foo(int p1, int p2, int p3, int p4, int p5, int p6, int p7) {} +int fun foo(int p1, int p2, int p3, int p4, int p5, int p6, int p7, int p8) {} // Noncompliant + +override int fun foo(int p1, int p2, int p3, int p4, int p5, int p6, int p7, int p8) {} // OK +private int fun foo(int p1, int p2, int p3, int p4, int p5, int p6, int p7, int p8) {} // Noncompliant +native [] {} int fun foo(int p1, int p2, int p3, int p4, int p5, int p6, int p7, int p8) {} // Noncompliant + +class A { + fun constructor(int p1, int p2, int p3, int p4, int p5, int p6, int p7, int p8) {} // Compliant, constructor +} diff --git a/sonar-go-checks/src/test/resources/checks/TooManyParameters.threshold.3.slang b/sonar-go-checks/src/test/resources/checks/TooManyParameters.threshold.3.slang new file mode 100644 index 00000000..4edb5ccf --- /dev/null +++ b/sonar-go-checks/src/test/resources/checks/TooManyParameters.threshold.3.slang @@ -0,0 +1,3 @@ +int fun foo(p1, p2, p3) {} +int fun foo(p1, p2, p3, p4, p5, p6, p7, p8) {} // Noncompliant {{This function has 8 parameters, which is greater than the 3 authorized.}} +// ^^^ ^^< ^^< ^^< ^^< ^^< diff --git a/sonar-go-checks/src/test/resources/checks/UnusedFunctionParameter.slang b/sonar-go-checks/src/test/resources/checks/UnusedFunctionParameter.slang new file mode 100644 index 00000000..0220e3a6 --- /dev/null +++ b/sonar-go-checks/src/test/resources/checks/UnusedFunctionParameter.slang @@ -0,0 +1,51 @@ +void fun fooBar() {} // OK + +void fun fooBar(int a) {} // Noncompliant {{Remove this unused function parameter "a".}} +// ^ + +void fun fooBar(int a, int b) {} // Noncompliant {{Remove these unused function parameters.}} +// ^ ^< {{Remove this unused method parameter b".}} +// ^@-1< {{Remove this unused method parameter a".}} + +void fun fooBar(int a, int b) { // Noncompliant {{Remove this unused function parameter "b".}} +// ^ + x = a; +} + +void fun fooBar(int a, int b) { // Noncompliant {{Remove this unused function parameter "a".}} +// ^ + match (b) { } +} + +void fun fooBar(int a, b) { // OK + x = a; + match (b) { } +} + +id fun fooBar(int id) {} // OK - issue is not raised here as 'id' is found in method header. + // This is done to avoid FP in languages that can use parameters in method headers (ex: in initializers for kotlin) + +class A { + + private fun constructor(int a) { // Compliant to prevent FP on constructor + } + + void fun fooBar() {} + + void fun fooBar(int a, int b) { // OK - method is not private and could potentially be overridden + x = a; + } + + private void fun fooBar(int a, int b) { // Noncompliant + x = a; + } + + private override void fun fooBar(int a, int b) { // OK - even though it is private, it is an override method + x = a; + } + +} + +void fun main(int a) {} // OK - "main" functions are ignored +int fun Main(int a) {} // OK - "main" functions are ignored +fun foo(p1, native [] {} p2) {p1} // OK - parameter with modifier are ignored \ No newline at end of file diff --git a/sonar-go-checks/src/test/resources/checks/UnusedLocalVariable.slang b/sonar-go-checks/src/test/resources/checks/UnusedLocalVariable.slang new file mode 100644 index 00000000..ced478cb --- /dev/null +++ b/sonar-go-checks/src/test/resources/checks/UnusedLocalVariable.slang @@ -0,0 +1,17 @@ +int var global; // Compliant + +void fun fooBar() { + int val a = 0; // Compliant + + int val b; // Noncompliant {{Remove this unused "b" local variable.}} + // ^ + + int var c; // Noncompliant {{Remove this unused "c" local variable.}} + // ^ + + int var d; // Compliant + d = 0; + + int var e; // Compliant + e = d + a; +} diff --git a/sonar-go-checks/src/test/resources/checks/UnusedPrivateMethod.slang b/sonar-go-checks/src/test/resources/checks/UnusedPrivateMethod.slang new file mode 100644 index 00000000..da249463 --- /dev/null +++ b/sonar-go-checks/src/test/resources/checks/UnusedPrivateMethod.slang @@ -0,0 +1,70 @@ +class A { + private fun unusedPrivate() {} // Noncompliant + + public fun unusedPrivate(int a) {} + + private fun unusedPrivate(int a) {} // Noncompliant + + private override fun unusedPrivateOverride(int a) {} // OK - might be forced to override the method here + + private fun usedPrivate(int a) {} + + fun unusedNoModifier(int a) { + usedPrivate(); + } + + public fun unusedPublic(int a) {} +} + +class B { + private fun f1() {} + + fun g(int f1) {} // FN of current approach, "f1" is used as identifier elsewhere in the class + + private fun f2() {} + + fun h(C c) { native[] { [c;][f2();] } } // FN of current approach, another class has same identifier for a method call "f2" (c.f2();) + + private fun writeObject() {} // Noncompliant +} + +class C { + + private fun constructor() {} // Compliant to prevent FP, constructor call semantic is not yet supported + + private fun unusedPrivate1() {} // Noncompliant + + private fun unusedPrivate2() {} // FN of current approach, "unusedPrivate2" identifier is used in inner class + + private fun unusedPrivate4() {} + + private fun f1() { + f1(); + usedInnerMethod1(); + usedInnerMethod2(); + unusedPrivate4(); + } + + class D { + private fun unusedPrivate2() {} + + private fun unusedPrivate3() {} // Noncompliant + + private fun unusedPrivate4() {} // FN of current approach, "unusedPrivate4" identifier is used in outer class + + private fun f1() {} // FN of current approach, "f1" is used in outer class + + private fun usedInnerMethod1() {} + + private fun f2() { + f2(); + unusedPrivate2(); + } + + class E { + private fun unusedPrivate5() {} // Noncompliant + + private fun usedInnerMethod2() {} + } + } +} diff --git a/sonar-go-checks/src/test/resources/checks/VariableAndParameterName.slang b/sonar-go-checks/src/test/resources/checks/VariableAndParameterName.slang new file mode 100644 index 00000000..3cddee71 --- /dev/null +++ b/sonar-go-checks/src/test/resources/checks/VariableAndParameterName.slang @@ -0,0 +1,32 @@ +var NOT_LOCAL; + +fun localVariables() { + var localVar; + var INVALID_LOCAL; // Noncompliant {{Rename this local variable to match the regular expression "^(_|[a-zA-Z0-9]+)$".}} +// ^^^^^^^^^^^^^ + var invalid_local; // Noncompliant +} + +fun parameters(param1, _PARAM2, param3) { // Noncompliant {{Rename this parameter to match the regular expression "^(_|[a-zA-Z0-9]+)$".}} +// ^^^^^^^ +} + +native [] { + [ + var POSSIBLY_NOT_LOCAL; + ] +}; + +class A { + fun constructor(param1, PARAM_2) { // Noncompliant +// ^^^^^^^ + } + fun method(param1, PARAM2_) { // Noncompliant +// ^^^^^^^ + } +} + +fun method(_) { } + +// testing corner case where the identifier syntax is not supported +fun method(__) { } diff --git a/sonar-go-checks/src/test/resources/checks/WrongAssignmentOperator.slang b/sonar-go-checks/src/test/resources/checks/WrongAssignmentOperator.slang new file mode 100644 index 00000000..25ebded3 --- /dev/null +++ b/sonar-go-checks/src/test/resources/checks/WrongAssignmentOperator.slang @@ -0,0 +1,27 @@ +target =-num; // Noncompliant {{Was "-=" meant instead?}} +// ^^ +target = + -num; +target = -num; // Compliant intent to assign inverse value of num is clear +target =--num; + +target += num; +target =+ num; // Noncompliant {{Was "+=" meant instead?}} +// ^^ +target = + + num; +target = + +num; +target = +num; +target =++num; +target=+num; // Compliant - no spaces between variable, operator and expression + +a = b != c; +a = b =! c; // Noncompliant {{Was "!=" meant instead?}} [[sc=11;ec=13]] +a = b =!! c; // Noncompliant +a = b = !c; +a =! c; // Noncompliant {{Add a space between "=" and "!" to avoid confusion.}} +a = ! c; +a = !c; +a = + !c; diff --git a/sonar-go-checks/src/test/resources/checks/fileheader/Compliant.slang b/sonar-go-checks/src/test/resources/checks/fileheader/Compliant.slang new file mode 100644 index 00000000..7c469c6c --- /dev/null +++ b/sonar-go-checks/src/test/resources/checks/fileheader/Compliant.slang @@ -0,0 +1,9 @@ +// copyright 2018 +// comment + +if (cond) { + a = b + 1 +}; + +x = 4; +x; \ No newline at end of file diff --git a/sonar-go-checks/src/test/resources/checks/fileheader/Multiline.slang b/sonar-go-checks/src/test/resources/checks/fileheader/Multiline.slang new file mode 100644 index 00000000..89950363 --- /dev/null +++ b/sonar-go-checks/src/test/resources/checks/fileheader/Multiline.slang @@ -0,0 +1,13 @@ +/* + * SonarSource SLang + * Copyright (C) 1999-2001 SonarSource SA + * mailto:info AT sonarsource DOT com + */ +// comment + +if (cond) { + a = b + 1 +}; + +x = 4; +x; diff --git a/sonar-go-checks/src/test/resources/checks/fileheader/NoFirstLine.slang b/sonar-go-checks/src/test/resources/checks/fileheader/NoFirstLine.slang new file mode 100644 index 00000000..357517fb --- /dev/null +++ b/sonar-go-checks/src/test/resources/checks/fileheader/NoFirstLine.slang @@ -0,0 +1,10 @@ + +// copyright 2018 + +if (cond) { + a = b + 1 +}; + +x = 4; +x; +// Noncompliant@0 {{Add or update the header of this file.}} \ No newline at end of file diff --git a/sonar-go-checks/src/test/resources/checks/fileheader/Noncompliant.slang b/sonar-go-checks/src/test/resources/checks/fileheader/Noncompliant.slang new file mode 100644 index 00000000..2570b181 --- /dev/null +++ b/sonar-go-checks/src/test/resources/checks/fileheader/Noncompliant.slang @@ -0,0 +1,9 @@ +// Noncompliant@0 {{Add or update the header of this file.}} +// comment + +if (cond) { + a = b + 1 +}; + +x = 4; +x; \ No newline at end of file diff --git a/sonar-go-commons/build.gradle.kts b/sonar-go-commons/build.gradle.kts new file mode 100644 index 00000000..a2089f56 --- /dev/null +++ b/sonar-go-commons/build.gradle.kts @@ -0,0 +1,38 @@ +/* + * SonarSource Go + * Copyright (C) 2018-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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/ + */ +plugins { + id("org.sonarsource.cloud-native.java-conventions") + id("org.sonarsource.cloud-native.code-style-conventions") + id("java-test-fixtures") +} + +dependencies { + compileOnly(libs.slf4j.api) + + implementation(libs.slang.api) + + testFixturesImplementation(libs.slang.api) + testFixturesImplementation(libs.assertj.core) + testFixturesImplementation(libs.mockito.core) + testFixturesImplementation(libs.sonar.analyzer.test.commons) + testFixturesImplementation(libs.classgraph) + testFixturesImplementation(libs.junit.jupiter.api) + testFixturesImplementation(libs.sonar.plugin.api.impl) + testFixturesImplementation(libs.sonar.plugin.api.test.fixtures) + + testRuntimeOnly(libs.junit.jupiter.engine) +} diff --git a/sonar-go-plugin/src/main/java/org/sonar/go/converter/ExternalProcessStreamConsumer.java b/sonar-go-commons/src/main/java/org/sonar/go/converter/ExternalProcessStreamConsumer.java similarity index 100% rename from sonar-go-plugin/src/main/java/org/sonar/go/converter/ExternalProcessStreamConsumer.java rename to sonar-go-commons/src/main/java/org/sonar/go/converter/ExternalProcessStreamConsumer.java diff --git a/sonar-go-plugin/src/main/java/org/sonar/go/converter/GoConverter.java b/sonar-go-commons/src/main/java/org/sonar/go/converter/GoConverter.java similarity index 100% rename from sonar-go-plugin/src/main/java/org/sonar/go/converter/GoConverter.java rename to sonar-go-commons/src/main/java/org/sonar/go/converter/GoConverter.java diff --git a/sonar-go-plugin/src/main/java/org/sonar/go/converter/package-info.java b/sonar-go-commons/src/main/java/org/sonar/go/converter/package-info.java similarity index 100% rename from sonar-go-plugin/src/main/java/org/sonar/go/converter/package-info.java rename to sonar-go-commons/src/main/java/org/sonar/go/converter/package-info.java diff --git a/sonar-go-plugin/src/test/java/org/sonar/go/testing/TestGoConverter.java b/sonar-go-commons/src/testFixtures/java/org/sonar/go/testing/TestGoConverter.java similarity index 100% rename from sonar-go-plugin/src/test/java/org/sonar/go/testing/TestGoConverter.java rename to sonar-go-commons/src/testFixtures/java/org/sonar/go/testing/TestGoConverter.java diff --git a/sonar-go-plugin/src/test/java/org/sonar/go/testing/TextRangeAssert.java b/sonar-go-commons/src/testFixtures/java/org/sonar/go/testing/TextRangeAssert.java similarity index 100% rename from sonar-go-plugin/src/test/java/org/sonar/go/testing/TextRangeAssert.java rename to sonar-go-commons/src/testFixtures/java/org/sonar/go/testing/TextRangeAssert.java diff --git a/sonar-go-plugin/build.gradle.kts b/sonar-go-plugin/build.gradle.kts index 96bf4306..b4a13cf3 100644 --- a/sonar-go-plugin/build.gradle.kts +++ b/sonar-go-plugin/build.gradle.kts @@ -29,8 +29,9 @@ plugins { dependencies { compileOnly(libs.sonar.plugin.api) + implementation(project(":sonar-go-checks")) + implementation(project(":sonar-go-commons")) implementation(libs.sonar.analyzer.commons) - implementation(libs.slang.checks) implementation(libs.slang.api) implementation(libs.checkstyle.import) implementation(libs.minimal.json) @@ -43,6 +44,7 @@ dependencies { testImplementation(libs.junit.jupiter.api) testImplementation(libs.sonar.plugin.api.impl) testImplementation(libs.sonar.plugin.api.test.fixtures) + testImplementation(testFixtures(project(":sonar-go-commons"))) testRuntimeOnly(libs.junit.jupiter.engine) } diff --git a/sonar-go-plugin/sonarpedia.json b/sonar-go-plugin/sonarpedia.json index 6d354db6..d8c72165 100644 --- a/sonar-go-plugin/sonarpedia.json +++ b/sonar-go-plugin/sonarpedia.json @@ -3,7 +3,7 @@ "languages": [ "GO" ], - "latest-update": "2024-11-26T13:00:59.627691847Z", + "latest-update": "2025-01-06T11:08:42.910728Z", "options": { "no-language-in-filenames": true, "preserve-filenames": true diff --git a/sonar-go-plugin/src/main/java/org/sonar/go/plugin/GoCheckList.java b/sonar-go-plugin/src/main/java/org/sonar/go/plugin/GoCheckList.java deleted file mode 100644 index c46c951a..00000000 --- a/sonar-go-plugin/src/main/java/org/sonar/go/plugin/GoCheckList.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * SonarSource Go - * Copyright (C) 2018-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. - * - * 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.go.plugin; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.List; -import org.sonar.go.checks.CodeAfterJumpGoCheck; -import org.sonar.go.checks.DuplicateBranchGoCheck; -import org.sonar.go.checks.OneStatementPerLineGoCheck; -import org.sonarsource.slang.checks.BadClassNameCheck; -import org.sonarsource.slang.checks.CheckList; -import org.sonarsource.slang.checks.CodeAfterJumpCheck; -import org.sonarsource.slang.checks.CollapsibleIfStatementsCheck; -import org.sonarsource.slang.checks.DuplicateBranchCheck; -import org.sonarsource.slang.checks.OneStatementPerLineCheck; -import org.sonarsource.slang.checks.TabsCheck; -import org.sonarsource.slang.checks.UnusedFunctionParameterCheck; -import org.sonarsource.slang.checks.UnusedLocalVariableCheck; -import org.sonarsource.slang.checks.UnusedPrivateMethodCheck; - -public class GoCheckList { - - private GoCheckList() { - // utility class - } - - static final Class[] GO_CHECK_BLACK_LIST = { - BadClassNameCheck.class, - // Can not enable rule S1066, as Go if-trees are containing an initializer, not well handled by SLang - CollapsibleIfStatementsCheck.class, - TabsCheck.class, - // Can not enable rule S1172 since it it not possible to identify overridden function with modifier (to avoid FP) - UnusedFunctionParameterCheck.class, - UnusedLocalVariableCheck.class, - UnusedPrivateMethodCheck.class, - // Replaced by language specific test - CodeAfterJumpCheck.class, - DuplicateBranchCheck.class, - OneStatementPerLineCheck.class - }; - - private static final Collection> GO_LANGUAGE_SPECIFIC_CHECKS = Arrays.asList( - CodeAfterJumpGoCheck.class, - DuplicateBranchGoCheck.class, - OneStatementPerLineGoCheck.class); - - public static List> checks() { - List> list = new ArrayList<>(CheckList.excludeChecks(GO_CHECK_BLACK_LIST)); - list.addAll(GO_LANGUAGE_SPECIFIC_CHECKS); - return list; - } -} diff --git a/sonar-go-plugin/src/main/java/org/sonar/go/plugin/GoRulesDefinition.java b/sonar-go-plugin/src/main/java/org/sonar/go/plugin/GoRulesDefinition.java index 0b759b13..5c185025 100644 --- a/sonar-go-plugin/src/main/java/org/sonar/go/plugin/GoRulesDefinition.java +++ b/sonar-go-plugin/src/main/java/org/sonar/go/plugin/GoRulesDefinition.java @@ -16,20 +16,14 @@ */ package org.sonar.go.plugin; -import java.lang.reflect.Field; -import java.util.Arrays; import java.util.List; import org.sonar.api.SonarRuntime; import org.sonar.api.server.rule.RulesDefinition; -import org.sonar.api.utils.AnnotationUtils; -import org.sonar.check.RuleProperty; +import org.sonar.go.checks.GoCheckList; import org.sonar.go.externalreport.AbstractReportSensor; import org.sonar.go.externalreport.GoLintReportSensor; import org.sonar.go.externalreport.GoVetReportSensor; import org.sonarsource.analyzer.commons.RuleMetadataLoader; -import org.sonarsource.slang.checks.utils.Language; -import org.sonarsource.slang.checks.utils.PropertyDefaultValue; -import org.sonarsource.slang.checks.utils.PropertyDefaultValues; public class GoRulesDefinition implements RulesDefinition { @@ -50,36 +44,9 @@ public void define(Context context) { List> checks = GoCheckList.checks(); metadataLoader.addRulesByAnnotatedClass(repository, checks); - setDefaultValuesForParameters(repository, checks, Language.GO); - repository.done(); AbstractReportSensor.createExternalRuleRepository(context, GoVetReportSensor.LINTER_ID, GoVetReportSensor.LINTER_NAME); AbstractReportSensor.createExternalRuleRepository(context, GoLintReportSensor.LINTER_ID, GoLintReportSensor.LINTER_NAME); } - - private static void setDefaultValuesForParameters(RulesDefinition.NewRepository repository, List> checks, Language language) { - for (Class check : checks) { - org.sonar.check.Rule ruleAnnotation = AnnotationUtils.getAnnotation(check, org.sonar.check.Rule.class); - String ruleKey = ruleAnnotation.key(); - for (Field field : check.getDeclaredFields()) { - RuleProperty ruleProperty = field.getAnnotation(RuleProperty.class); - PropertyDefaultValues defaultValues = field.getAnnotation(PropertyDefaultValues.class); - if (ruleProperty == null || defaultValues == null) { - continue; - } - String paramKey = ruleProperty.key(); - - List valueForLanguage = Arrays.stream(defaultValues.value()) - .filter(defaultValue -> defaultValue.language() == language) - .toList(); - if (valueForLanguage.size() != 1) { - throw new IllegalStateException("Invalid @PropertyDefaultValue on " + check.getSimpleName() + - " for language " + language); - } - valueForLanguage - .forEach(defaultValue -> repository.rule(ruleKey).param(paramKey).setDefaultValue(defaultValue.defaultValue())); - } - } - } } diff --git a/sonar-go-plugin/src/main/java/org/sonar/go/plugin/GoSensor.java b/sonar-go-plugin/src/main/java/org/sonar/go/plugin/GoSensor.java index 0f5f5fce..7674d4d9 100644 --- a/sonar-go-plugin/src/main/java/org/sonar/go/plugin/GoSensor.java +++ b/sonar-go-plugin/src/main/java/org/sonar/go/plugin/GoSensor.java @@ -24,6 +24,7 @@ import org.sonar.api.batch.sensor.SensorDescriptor; import org.sonar.api.issue.NoSonarFilter; import org.sonar.api.measures.FileLinesContextFactory; +import org.sonar.go.checks.GoCheckList; import org.sonar.go.converter.GoConverter; import org.sonarsource.slang.api.ASTConverter; import org.sonarsource.slang.api.NativeTree; diff --git a/sonar-go-plugin/src/main/java/org/sonar/go/plugin/InputFileContext.java b/sonar-go-plugin/src/main/java/org/sonar/go/plugin/InputFileContext.java index fa046162..09782b49 100644 --- a/sonar-go-plugin/src/main/java/org/sonar/go/plugin/InputFileContext.java +++ b/sonar-go-plugin/src/main/java/org/sonar/go/plugin/InputFileContext.java @@ -36,7 +36,7 @@ public class InputFileContext extends TreeContext { - private static final String PARSING_ERROR_RULE_KEY = "ParsingError"; + private static final String PARSING_ERROR_RULE_KEY = "S2260"; private Map> filteredRules = new HashMap<>(); public final SensorContext sensorContext; diff --git a/sonar-go-plugin/src/main/java/org/sonar/go/plugin/MetricVisitor.java b/sonar-go-plugin/src/main/java/org/sonar/go/plugin/MetricVisitor.java index bc9b4843..b8474bf3 100644 --- a/sonar-go-plugin/src/main/java/org/sonar/go/plugin/MetricVisitor.java +++ b/sonar-go-plugin/src/main/java/org/sonar/go/plugin/MetricVisitor.java @@ -24,13 +24,13 @@ import org.sonar.api.measures.CoreMetrics; import org.sonar.api.measures.FileLinesContext; import org.sonar.api.measures.FileLinesContextFactory; +import org.sonar.go.checks.complexity.CognitiveComplexity; import org.sonarsource.slang.api.BlockTree; import org.sonarsource.slang.api.ClassDeclarationTree; import org.sonarsource.slang.api.Comment; import org.sonarsource.slang.api.FunctionDeclarationTree; import org.sonarsource.slang.api.TopLevelTree; import org.sonarsource.slang.api.Tree; -import org.sonarsource.slang.checks.complexity.CognitiveComplexity; import org.sonarsource.slang.visitors.TreeVisitor; public class MetricVisitor extends TreeVisitor { diff --git a/sonar-go-plugin/src/main/resources/org/sonar/l10n/go/rules/go/S1451.json b/sonar-go-plugin/src/main/resources/org/sonar/l10n/go/rules/go/S1451.json index 2ce43510..c5cba03b 100644 --- a/sonar-go-plugin/src/main/resources/org/sonar/l10n/go/rules/go/S1451.json +++ b/sonar-go-plugin/src/main/resources/org/sonar/l10n/go/rules/go/S1451.json @@ -12,7 +12,9 @@ "func": "Constant\/Issue", "constantCost": "5min" }, - "tags": [], + "tags": [ + "convention" + ], "defaultSeverity": "Blocker", "ruleSpecification": "RSPEC-1451", "sqKey": "S1451", diff --git a/sonar-go-plugin/src/main/resources/org/sonar/l10n/go/rules/go/S1764.json b/sonar-go-plugin/src/main/resources/org/sonar/l10n/go/rules/go/S1764.json index 31ccc4e1..b9b0774c 100644 --- a/sonar-go-plugin/src/main/resources/org/sonar/l10n/go/rules/go/S1764.json +++ b/sonar-go-plugin/src/main/resources/org/sonar/l10n/go/rules/go/S1764.json @@ -12,7 +12,9 @@ "func": "Constant\/Issue", "constantCost": "2min" }, - "tags": [], + "tags": [ + "suspicious" + ], "defaultSeverity": "Major", "ruleSpecification": "RSPEC-1764", "sqKey": "S1764", diff --git a/sonar-go-plugin/src/main/resources/org/sonar/l10n/go/rules/go/ParsingError.html b/sonar-go-plugin/src/main/resources/org/sonar/l10n/go/rules/go/S2260.html similarity index 100% rename from sonar-go-plugin/src/main/resources/org/sonar/l10n/go/rules/go/ParsingError.html rename to sonar-go-plugin/src/main/resources/org/sonar/l10n/go/rules/go/S2260.html diff --git a/sonar-go-plugin/src/main/resources/org/sonar/l10n/go/rules/go/ParsingError.json b/sonar-go-plugin/src/main/resources/org/sonar/l10n/go/rules/go/S2260.json similarity index 100% rename from sonar-go-plugin/src/main/resources/org/sonar/l10n/go/rules/go/ParsingError.json rename to sonar-go-plugin/src/main/resources/org/sonar/l10n/go/rules/go/S2260.json diff --git a/sonar-go-plugin/src/main/resources/org/sonar/l10n/go/rules/go/Sonar_way_profile.json b/sonar-go-plugin/src/main/resources/org/sonar/l10n/go/rules/go/Sonar_way_profile.json index 0b4d98f7..92599200 100644 --- a/sonar-go-plugin/src/main/resources/org/sonar/l10n/go/rules/go/Sonar_way_profile.json +++ b/sonar-go-plugin/src/main/resources/org/sonar/l10n/go/rules/go/Sonar_way_profile.json @@ -1,7 +1,6 @@ { "name": "Sonar way", "ruleKeys": [ - "ParsingError", "S100", "S107", "S108", @@ -21,6 +20,7 @@ "S1871", "S1940", "S2068", + "S2260", "S2757", "S3776", "S3923", diff --git a/sonar-go-plugin/src/test/java/org/sonar/go/checks/README.md b/sonar-go-plugin/src/test/java/org/sonar/go/checks/README.md deleted file mode 100644 index 1ad1a21b..00000000 --- a/sonar-go-plugin/src/test/java/org/sonar/go/checks/README.md +++ /dev/null @@ -1 +0,0 @@ -Here language-specific check implementations should be stored. \ No newline at end of file diff --git a/sonar-go-plugin/src/test/java/org/sonar/go/plugin/GoCheckListTest.java b/sonar-go-plugin/src/test/java/org/sonar/go/plugin/GoCheckListTest.java index 0eff766c..b4796843 100644 --- a/sonar-go-plugin/src/test/java/org/sonar/go/plugin/GoCheckListTest.java +++ b/sonar-go-plugin/src/test/java/org/sonar/go/plugin/GoCheckListTest.java @@ -16,62 +16,102 @@ */ package org.sonar.go.plugin; -import io.github.classgraph.ClassGraph; -import io.github.classgraph.ClassInfo; -import io.github.classgraph.ScanResult; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; -import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; +import org.sonar.go.checks.AllBranchesIdenticalCheck; +import org.sonar.go.checks.BadFunctionNameCheck; +import org.sonar.go.checks.BooleanInversionCheck; +import org.sonar.go.checks.BooleanLiteralCheck; +import org.sonar.go.checks.CodeAfterJumpGoCheck; +import org.sonar.go.checks.DuplicateBranchGoCheck; +import org.sonar.go.checks.DuplicatedFunctionImplementationCheck; +import org.sonar.go.checks.ElseIfWithoutElseCheck; +import org.sonar.go.checks.EmptyBlockCheck; +import org.sonar.go.checks.EmptyCommentCheck; +import org.sonar.go.checks.EmptyFunctionCheck; +import org.sonar.go.checks.FileHeaderCheck; +import org.sonar.go.checks.FixMeCommentCheck; +import org.sonar.go.checks.FunctionCognitiveComplexityCheck; +import org.sonar.go.checks.GoCheckList; +import org.sonar.go.checks.HardcodedCredentialsCheck; +import org.sonar.go.checks.HardcodedIpCheck; +import org.sonar.go.checks.IdenticalBinaryOperandCheck; +import org.sonar.go.checks.IdenticalConditionsCheck; +import org.sonar.go.checks.IfConditionalAlwaysTrueOrFalseCheck; +import org.sonar.go.checks.MatchCaseTooBigCheck; +import org.sonar.go.checks.MatchWithoutElseCheck; +import org.sonar.go.checks.NestedMatchCheck; +import org.sonar.go.checks.OctalValuesCheck; +import org.sonar.go.checks.OneStatementPerLineGoCheck; +import org.sonar.go.checks.ParsingErrorCheck; +import org.sonar.go.checks.RedundantParenthesesCheck; +import org.sonar.go.checks.SelfAssignmentCheck; +import org.sonar.go.checks.StringLiteralDuplicatedCheck; +import org.sonar.go.checks.TodoCommentCheck; +import org.sonar.go.checks.TooComplexExpressionCheck; +import org.sonar.go.checks.TooDeeplyNestedStatementsCheck; +import org.sonar.go.checks.TooLongFunctionCheck; +import org.sonar.go.checks.TooLongLineCheck; +import org.sonar.go.checks.TooManyCasesCheck; +import org.sonar.go.checks.TooManyLinesOfCodeFileCheck; +import org.sonar.go.checks.TooManyParametersCheck; +import org.sonar.go.checks.VariableAndParameterNameCheck; +import org.sonar.go.checks.WrongAssignmentOperatorCheck; import static org.assertj.core.api.Assertions.assertThat; class GoCheckListTest { - private static final String GO_CHECKS_PACKAGE = "org.sonar.go.checks"; - @Test - void go_checks_size() { - Assertions.assertThat(GoCheckList.checks()).hasSize(38); + void shouldVerifyChecksSize() { + assertThat(GoCheckList.checks()).hasSize(38); } @Test - void go_specific_checks_are_added_to_check_list() { - List checkListNames = GoCheckList.checks().stream().map(Class::getName).collect(Collectors.toList()); - List languageImplementation = findSlangChecksInPackage(GO_CHECKS_PACKAGE); - for (String languageCheck : languageImplementation) { - assertThat(checkListNames).contains(languageCheck); - assertThat(languageCheck).endsWith("GoCheck"); - } + void shouldContainParsingErrorCheck() { + assertThat(GoCheckList.checks()).contains(ParsingErrorCheck.class); } @Test - void go_excluded_not_present() { - List> checks = GoCheckList.checks(); - assertThat(checks) - .doesNotContain(GoCheckList.GO_CHECK_BLACK_LIST) - .isNotEmpty(); - } + void shouldContainsClasses() { + assertThat(GoCheckList.checks()).containsOnly( - /** - * Returns the fully qualified names (FQNs) of the classes inside @packageName implementing SlangCheck. - * @param packageName Used to filter classes - the FQN of a class contains the package name. - * @return A list of slang checks (FQNs). - */ - private static List findSlangChecksInPackage(String packageName) { - try (ScanResult scanResult = new ClassGraph().enableAllInfo().acceptPackages(packageName).scan()) { - Map allClasses = scanResult.getAllClassesAsMap(); - List testClassesInPackage = new ArrayList<>(); - for (Map.Entry classInfoEntry : allClasses.entrySet()) { - String name = classInfoEntry.getKey(); - ClassInfo classInfo = classInfoEntry.getValue(); - if (name.startsWith(packageName) && classInfo.getInterfaces().stream().anyMatch(i -> i.getSimpleName().equals("SlangCheck"))) { - testClassesInPackage.add(classInfo.getName()); - } - } - return testClassesInPackage; - } + AllBranchesIdenticalCheck.class, + BadFunctionNameCheck.class, + BooleanInversionCheck.class, + BooleanLiteralCheck.class, + CodeAfterJumpGoCheck.class, + DuplicateBranchGoCheck.class, + DuplicatedFunctionImplementationCheck.class, + ElseIfWithoutElseCheck.class, + EmptyBlockCheck.class, + EmptyCommentCheck.class, + EmptyFunctionCheck.class, + FileHeaderCheck.class, + FixMeCommentCheck.class, + FunctionCognitiveComplexityCheck.class, + HardcodedCredentialsCheck.class, + HardcodedIpCheck.class, + IdenticalBinaryOperandCheck.class, + IdenticalConditionsCheck.class, + IfConditionalAlwaysTrueOrFalseCheck.class, + MatchCaseTooBigCheck.class, + MatchWithoutElseCheck.class, + NestedMatchCheck.class, + OctalValuesCheck.class, + OneStatementPerLineGoCheck.class, + ParsingErrorCheck.class, + RedundantParenthesesCheck.class, + SelfAssignmentCheck.class, + StringLiteralDuplicatedCheck.class, + TodoCommentCheck.class, + TooComplexExpressionCheck.class, + TooDeeplyNestedStatementsCheck.class, + TooLongFunctionCheck.class, + TooLongLineCheck.class, + TooManyLinesOfCodeFileCheck.class, + TooManyCasesCheck.class, + TooManyParametersCheck.class, + VariableAndParameterNameCheck.class, + WrongAssignmentOperatorCheck.class); } } diff --git a/sonar-go-plugin/src/test/java/org/sonar/go/plugin/GoRulesDefinitionTest.java b/sonar-go-plugin/src/test/java/org/sonar/go/plugin/GoRulesDefinitionTest.java index 52369008..10d2786e 100644 --- a/sonar-go-plugin/src/test/java/org/sonar/go/plugin/GoRulesDefinitionTest.java +++ b/sonar-go-plugin/src/test/java/org/sonar/go/plugin/GoRulesDefinitionTest.java @@ -27,6 +27,7 @@ import org.sonar.api.server.debt.DebtRemediationFunction; import org.sonar.api.server.rule.RulesDefinition; import org.sonar.api.utils.Version; +import org.sonar.go.checks.GoCheckList; import org.sonar.go.externalreport.ExternalKeyUtils; import static org.assertj.core.api.Assertions.assertThat; diff --git a/sonar-go-plugin/src/test/java/org/sonar/go/plugin/GoSensorTest.java b/sonar-go-plugin/src/test/java/org/sonar/go/plugin/GoSensorTest.java index 763fec9c..41d0b93e 100644 --- a/sonar-go-plugin/src/test/java/org/sonar/go/plugin/GoSensorTest.java +++ b/sonar-go-plugin/src/test/java/org/sonar/go/plugin/GoSensorTest.java @@ -53,6 +53,7 @@ import org.sonar.api.rule.RuleKey; import org.sonar.api.testfixtures.log.LogTesterJUnit5; import org.sonar.api.utils.Version; +import org.sonar.go.checks.GoCheckList; import org.sonar.go.converter.GoConverter; import org.sonarsource.slang.checks.api.SlangCheck; diff --git a/sonar-go-plugin/src/test/java/org/sonar/go/plugin/SlangSensorTest.java b/sonar-go-plugin/src/test/java/org/sonar/go/plugin/SlangSensorTest.java index 0f94f6e4..16376241 100644 --- a/sonar-go-plugin/src/test/java/org/sonar/go/plugin/SlangSensorTest.java +++ b/sonar-go-plugin/src/test/java/org/sonar/go/plugin/SlangSensorTest.java @@ -50,6 +50,8 @@ import org.sonar.api.resources.Language; import org.sonar.api.rule.RuleKey; import org.sonar.api.utils.Version; +import org.sonar.go.checks.IdenticalBinaryOperandCheck; +import org.sonar.go.checks.StringLiteralDuplicatedCheck; import org.sonar.go.converter.GoConverter; import org.sonar.go.plugin.caching.DummyReadCache; import org.sonar.go.plugin.caching.DummyWriteCache; @@ -57,8 +59,6 @@ import org.sonarsource.slang.api.ASTConverter; import org.sonarsource.slang.api.TopLevelTree; import org.sonarsource.slang.api.Tree; -import org.sonarsource.slang.checks.IdenticalBinaryOperandCheck; -import org.sonarsource.slang.checks.StringLiteralDuplicatedCheck; import org.sonarsource.slang.checks.api.SlangCheck; import static org.assertj.core.api.Assertions.assertThat; @@ -254,13 +254,13 @@ fun x() {} fun y() {}\ """); context.fileSystem().add(inputFile); - CheckFactory checkFactory = checkFactory("ParsingError"); + CheckFactory checkFactory = checkFactory("S2260"); sensor(checkFactory).execute(context); Collection issues = context.allIssues(); assertThat(issues).hasSize(1); Issue issue = issues.iterator().next(); - assertThat(issue.ruleKey().rule()).isEqualTo("ParsingError"); + assertThat(issue.ruleKey().rule()).isEqualTo("S2260"); IssueLocation location = issue.primaryLocation(); assertThat(location.inputComponent()).isEqualTo(inputFile); assertThat(location.message()).isEqualTo("A parsing error occurred in this file.");