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.