1
votes

I am developing a plugin for SonarQube 5.6.6 using the Java Plugin API. I've created some custom rules (checks) and one of them reports the same issue in the same line several times. This makes sense because that line has the same error multiple times but, is there any way to limit this so SonarQube only shows an issue in that line?

Images (and code) speak louder than words, so I'll show an example: a check that reports an issue every time it detects a new class.

@Rule(key = "foo_key", name = "Foo issue", description = "Foo issue", priority = Priority.INFO)
public class FooCheck extends IssuableSubscriptionVisitor {

    @Override
    public List<Kind> nodesToVisit() {
        return ImmutableList.of(Kind.NEW_CLASS);
    }

    @Override
    public void visitNode(Tree tree) {
        reportIssue(tree, "New class!");
    }

}

Therefore, we'll get two issues in a line like Foo foo = new Foo(new Bar());:

The same issue in the same line

I know that I could change this specific check to achieve what I want. For example, I could avoid reporting an issue when analysing a NEW_CLASS node if any of its arguments is another NEW_CLASS; in this way, we would report an issue on node new Bar(), but no on node new Foo(new Bar()), so we would get only an issue:

@Override
public void visitNode(Tree tree) {
    final NewClassTree newClassTree = (NewClassTree) tree;

    if (newClassTree.arguments().stream().noneMatch(arg -> arg.is(Kind.NEW_CLASS))) {
        reportIssue(tree, "New class!");
    }
}

However, this is a solution for just this check. I want to know if there is a general way to tell SonarQube not to show several issues of the same check per line.

Thanks.

2

2 Answers

2
votes

You can get the line number of your tree node before reporting the issue, saving it to a global variable lastLineReported or a list of reported lines. Then, you check with a simple if statement in two ways:

1 - Using lastLineReported variable:

if(lastLineReported != currentLine) {
        lastLineReported = currentLine;
        reportIssue(tree, "New class!");
    }

2 - Using list of lines reported:

if(!reportedLines.contains(currentLine)) {
        reportedLines.add(currentLine);
        reportIssue(tree, "New class!");
    }
1
votes

Based on Rafael Costa's answer, I will provide the complete solution:

The key is that interface Tree (org.sonar.plugins.java.api.tree.Tree) has a method firstToken() that returns a SyntaxToken, which is another interface that has a method line(). Therefore, we can call tree.firstToken().line() to get the line of the node we are visiting, save it the first time we report an issue on that line and check, in later visits to nodes, if their lines already have a reported issue.

Pay attention: we mustn't save statically these lines in a collection, since the value of this collection will be shared in every visit to a node of every file of source code to be analysed. Instead, we must save each line along with the file we are analysing. If we didn't do this and we created an issue in the line X of a file A, if file B had an issue in its line X, this issue wouldn't be created.

@Rule(key = "foo_key", name = "Foo issue", description = "Foo issue", priority = Priority.INFO)
public class FooCheck extends IssuableSubscriptionVisitor {

    private static final Map<String, Collection<Integer>> linesWithIssuesByClass = new HashMap<>();

    @Override
    public List<Kind> nodesToVisit() {
        return ImmutableList.of(Kind.NEW_CLASS);
    }

    @Override
    public void visitNode(Tree tree) {
        if (lineAlreadyHasThisIssue(tree)) {
            return;
        }

        reportIssue(tree);
    }

    private boolean lineAlreadyHasThisIssue(Tree tree) {
        if (tree.firstToken() != null) {
            final String classname = getFullyQualifiedNameOfClassOf(tree);
            final int line = tree.firstToken().line();

            return linesWithIssuesByClass.containsKey(classname)
                    && linesWithIssuesByClass.get(classname).contains(line);
        }

        return false;
    }

    private void reportIssue(Tree tree) {
        if (tree.firstToken() != null) {
            final String classname = getFullyQualifiedNameOfClassOf(tree);
            final int line = tree.firstToken().line();

            if (!linesWithIssuesByClass.containsKey(classname)) {
                linesWithIssuesByClass.put(classname, new ArrayList<>());
            }

            linesWithIssuesByClass.get(classname).add(line);
        }

        reportIssue(tree, "New class!");
    }

    private String getFullyQualifiedNameOfClassOf(Tree tree) {
        Tree parent = tree.parent();

        while (parent != null) {
            final Tree grandparent = parent.parent();

            if (parent.is(Kind.CLASS) && grandparent != null && grandparent.is(Kind.COMPILATION_UNIT)) {
                final String packageName = getPackageName((CompilationUnitTree) grandparent);

                return packageName.isEmpty()
                        ? getClassName((ClassTree) parent)
                        : packageName + '.' + getClassName((ClassTree) parent);
            }

            parent = parent.parent();
        }

        return "";
    }

    private String getPackageName(CompilationUnitTree compilationUnitTree) {
        final PackageDeclarationTree packageDeclarationTree = compilationUnitTree.packageDeclaration();
        if (packageDeclarationTree == null) {
            return "";
        }

        return packageDeclarationTree.packageName().toString();
    }

    private String getClassName(ClassTree classTree) {
        final IdentifierTree simpleName = classTree.simpleName();
        return simpleName == null
                ? ""
                : simpleName.toString();
    }

}

I check that tree.firstToken() is not null because interface Tree has this code:

@Nullable
SyntaxToken firstToken();

Though Tree has another method lastToken() that returns another SyntaxToken with the line, we should call to firstToken() because our node might be multiline like this:

Foo foo = new Foo(
        new Bar()
);

and lastToken().line() would have different values in each visit to the node, while firstToken().line() wouldn't.