diff --git a/src/main/java/fr/inria/corese/core/next/data/api/term/Literal.java b/src/main/java/fr/inria/corese/core/next/data/api/term/Literal.java index cf37609da..e3a1d23fd 100644 --- a/src/main/java/fr/inria/corese/core/next/data/api/term/Literal.java +++ b/src/main/java/fr/inria/corese/core/next/data/api/term/Literal.java @@ -22,6 +22,7 @@ public interface Literal extends Value { /** * @return the lexical value of the literal */ + @Override String getLabel(); /** @@ -52,6 +53,7 @@ public interface Literal extends Value { /** * @return the value of the literal as an int if possible */ + @Override int intValue(); /** @@ -77,6 +79,7 @@ public interface Literal extends Value { /** * @return the value of the literal as a double if possible */ + @Override double doubleValue(); /** diff --git a/src/main/java/fr/inria/corese/core/next/data/api/term/SimpleDate.java b/src/main/java/fr/inria/corese/core/next/data/api/term/SimpleDate.java index 5b53b944e..5b1997eba 100644 --- a/src/main/java/fr/inria/corese/core/next/data/api/term/SimpleDate.java +++ b/src/main/java/fr/inria/corese/core/next/data/api/term/SimpleDate.java @@ -23,7 +23,7 @@ public final class SimpleDate extends AbstractTemporalPointLiteral { private final XMLGregorianCalendar calendar; public SimpleDate(String lexicalValue) { - this(lexicalValue, XSDDatatype.DATE.getIRI(), XSDDatatype.DATE); + this(lexicalValue, XSDDatatype.DATE.getIRI()); } public SimpleDate(XMLGregorianCalendar calendar) { @@ -33,11 +33,6 @@ public SimpleDate(XMLGregorianCalendar calendar) { } public SimpleDate(String lexicalValue, IRI datatype) { - this(lexicalValue, datatype, null); - } - - @SuppressWarnings("java:S1172") // Kept for source compatibility with the temporal literal API. - public SimpleDate(String lexicalValue, IRI datatype, CoreDatatype coreDatatype) { super(datatype == null ? XSDDatatype.DATE.getIRI() : datatype); this.label = Objects.requireNonNull(lexicalValue, "lexicalValue"); this.calendar = DatatypeFactory.newDefaultInstance().newXMLGregorianCalendar(lexicalValue); diff --git a/src/main/java/fr/inria/corese/core/next/data/api/term/SimpleDateTime.java b/src/main/java/fr/inria/corese/core/next/data/api/term/SimpleDateTime.java index 5a1c0c84c..957ff29fa 100644 --- a/src/main/java/fr/inria/corese/core/next/data/api/term/SimpleDateTime.java +++ b/src/main/java/fr/inria/corese/core/next/data/api/term/SimpleDateTime.java @@ -23,7 +23,7 @@ public final class SimpleDateTime extends AbstractTemporalPointLiteral { private final XMLGregorianCalendar calendar; public SimpleDateTime(String lexicalValue) { - this(lexicalValue, XSDDatatype.DATETIME.getIRI(), XSDDatatype.DATETIME); + this(lexicalValue, XSDDatatype.DATETIME.getIRI()); } public SimpleDateTime(XMLGregorianCalendar calendar) { @@ -33,11 +33,6 @@ public SimpleDateTime(XMLGregorianCalendar calendar) { } public SimpleDateTime(String lexicalValue, IRI datatype) { - this(lexicalValue, datatype, null); - } - - @SuppressWarnings("java:S1172") // Kept for source compatibility with the temporal literal API. - public SimpleDateTime(String lexicalValue, IRI datatype, CoreDatatype coreDatatype) { super(datatype == null ? XSDDatatype.DATETIME.getIRI() : datatype); this.label = Objects.requireNonNull(lexicalValue, "lexicalValue"); this.calendar = DatatypeFactory.newDefaultInstance().newXMLGregorianCalendar(lexicalValue); diff --git a/src/main/java/fr/inria/corese/core/next/data/api/term/SimpleDuration.java b/src/main/java/fr/inria/corese/core/next/data/api/term/SimpleDuration.java index 65328f2a7..4855eb280 100644 --- a/src/main/java/fr/inria/corese/core/next/data/api/term/SimpleDuration.java +++ b/src/main/java/fr/inria/corese/core/next/data/api/term/SimpleDuration.java @@ -43,7 +43,7 @@ private void readObject(ObjectInputStream input) throws IOException, ClassNotFou } public SimpleDuration(String lexicalValue) { - this(lexicalValue, XSDDatatype.DURATION.getIRI(), XSDDatatype.DURATION); + this(lexicalValue, XSDDatatype.DURATION.getIRI()); } public SimpleDuration(TemporalAmount temporalAmount) { @@ -53,12 +53,7 @@ public SimpleDuration(TemporalAmount temporalAmount) { } public SimpleDuration(String lexicalValue, IRI datatype) { - this(lexicalValue, datatype, null); - } - - @SuppressWarnings("java:S1172") // Kept for source compatibility with the temporal literal API. - public SimpleDuration(String lexicalValue, IRI datatype, CoreDatatype coreDatatype) { - super(); + super(datatype); this.label = Objects.requireNonNull(lexicalValue, "lexicalValue"); this.temporalAmount = parseTemporalAmount(lexicalValue); } diff --git a/src/main/java/fr/inria/corese/core/next/data/api/term/SimpleTime.java b/src/main/java/fr/inria/corese/core/next/data/api/term/SimpleTime.java index d4ab34fbe..cfa0701df 100644 --- a/src/main/java/fr/inria/corese/core/next/data/api/term/SimpleTime.java +++ b/src/main/java/fr/inria/corese/core/next/data/api/term/SimpleTime.java @@ -23,7 +23,7 @@ public final class SimpleTime extends AbstractTemporalPointLiteral { private final XMLGregorianCalendar calendar; public SimpleTime(String lexicalValue) { - this(lexicalValue, XSDDatatype.TIME.getIRI(), XSDDatatype.TIME); + this(lexicalValue, XSDDatatype.TIME.getIRI()); } public SimpleTime(XMLGregorianCalendar calendar) { @@ -33,11 +33,6 @@ public SimpleTime(XMLGregorianCalendar calendar) { } public SimpleTime(String lexicalValue, IRI datatype) { - this(lexicalValue, datatype, null); - } - - @SuppressWarnings("java:S1172") // Kept for source compatibility with the temporal literal API. - public SimpleTime(String lexicalValue, IRI datatype, CoreDatatype coreDatatype) { super(datatype == null ? XSDDatatype.TIME.getIRI() : datatype); this.label = Objects.requireNonNull(lexicalValue, "lexicalValue"); this.calendar = DatatypeFactory.newDefaultInstance().newXMLGregorianCalendar(lexicalValue); diff --git a/src/main/java/fr/inria/corese/core/next/data/api/term/SimpleTriple.java b/src/main/java/fr/inria/corese/core/next/data/api/term/SimpleTriple.java index f03ddcb4f..4f7403b81 100644 --- a/src/main/java/fr/inria/corese/core/next/data/api/term/SimpleTriple.java +++ b/src/main/java/fr/inria/corese/core/next/data/api/term/SimpleTriple.java @@ -39,7 +39,6 @@ public boolean equals(Object other) { } @Override - @SuppressWarnings("NullableProblems") public String toString() { return "<<(" + subject + " " + predicate + " " + object + ")>>"; } diff --git a/src/main/java/fr/inria/corese/core/next/data/impl/io/jsonld/package-info.java b/src/main/java/fr/inria/corese/core/next/data/impl/io/jsonld/package-info.java deleted file mode 100644 index df9025a34..000000000 --- a/src/main/java/fr/inria/corese/core/next/data/impl/io/jsonld/package-info.java +++ /dev/null @@ -1,4 +0,0 @@ -/** - * Configuration shared by the JSON-LD parser and serializer implementations. - */ -package fr.inria.corese.core.next.data.impl.io.jsonld; diff --git a/src/main/java/fr/inria/corese/core/next/data/impl/io/parser/rdfa/RDFaParser.java b/src/main/java/fr/inria/corese/core/next/data/impl/io/parser/rdfa/RDFaParser.java index ef086a53d..dd02d636d 100644 --- a/src/main/java/fr/inria/corese/core/next/data/impl/io/parser/rdfa/RDFaParser.java +++ b/src/main/java/fr/inria/corese/core/next/data/impl/io/parser/rdfa/RDFaParser.java @@ -172,10 +172,10 @@ private void clearAllCharactersBuffers() { } } - /* - * The algorithm in W3C recommendation is based on DOM processing, but this implementation is made in SAX. + /** + * The algorithm in W3C recommendation is based on DOM processing, but this implementation is made in SAX. * To reconcile both approaches, the "local values" are stored in a pile of ProcessingContext. The IRI mapping are shared independently. - * All operations except the ones that create literals are done ine this function. + * All operations except the ones that create literals are done in this function. */ @SuppressWarnings({"java:S3776", "java:S3398", "java:S125"}) private void startProcessElement(String qName, Attributes attrs) { diff --git a/src/main/java/fr/inria/corese/core/next/data/impl/io/parser/rdfxml/RDFXMLStatementEmitter.java b/src/main/java/fr/inria/corese/core/next/data/impl/io/parser/rdfxml/RDFXMLStatementEmitter.java index 1b86b0361..6981fed3c 100644 --- a/src/main/java/fr/inria/corese/core/next/data/impl/io/parser/rdfxml/RDFXMLStatementEmitter.java +++ b/src/main/java/fr/inria/corese/core/next/data/impl/io/parser/rdfxml/RDFXMLStatementEmitter.java @@ -11,6 +11,7 @@ import org.xml.sax.Attributes; import java.util.Optional; +import java.util.regex.Pattern; import static fr.inria.corese.core.next.data.impl.io.parser.rdfxml.RDFXMLUtils.*; @@ -22,6 +23,8 @@ public class RDFXMLStatementEmitter { private static final Logger logger = LoggerFactory.getLogger(RDFXMLStatementEmitter.class); + private static final Pattern CONTAINER_MEMBERSHIP_PATTERN = Pattern.compile("^_\\d+$"); + private final Model model; private final ValueFactory factory; @@ -138,7 +141,7 @@ public void emitPropertyAttribute(Resource subject, Attributes attrs) { "rdf:li cannot be used as property attribute. " + "It can only be used as property element inside containers."); } - if (attrLocal.matches("^_\\d+$")) { + if (CONTAINER_MEMBERSHIP_PATTERN.matcher(attrLocal).matches()) { throw new ParsingException( "rdf:" + attrLocal + " cannot be used as property attribute. " + "Container membership properties can only be used as property elements."); diff --git a/src/main/java/fr/inria/corese/core/next/data/impl/model/EmptyModel.java b/src/main/java/fr/inria/corese/core/next/data/impl/model/EmptyModel.java index 00b302f99..702979984 100644 --- a/src/main/java/fr/inria/corese/core/next/data/impl/model/EmptyModel.java +++ b/src/main/java/fr/inria/corese/core/next/data/impl/model/EmptyModel.java @@ -62,7 +62,6 @@ public Optional removeNamespace(String prefix) { // -- Statement operations: read-only and always empty -- @Override - @SuppressWarnings("NullableProblems") public Iterator iterator() { return emptySet.iterator(); } diff --git a/src/main/java/fr/inria/corese/core/next/data/spi/model/AbstractModel.java b/src/main/java/fr/inria/corese/core/next/data/spi/model/AbstractModel.java index 5180eee40..e7b952e37 100644 --- a/src/main/java/fr/inria/corese/core/next/data/spi/model/AbstractModel.java +++ b/src/main/java/fr/inria/corese/core/next/data/spi/model/AbstractModel.java @@ -263,7 +263,6 @@ private Statement findNext() { } @Override - @SuppressWarnings("NullableProblems") public Iterator iterator() { return new ValueSetIterator(AbstractModel.this.iterator()); } @@ -308,7 +307,6 @@ public boolean removeAll(Collection collection) { } @Override - @SuppressWarnings("NullableProblems") public Object[] toArray() { Iterator iterator = AbstractModel.this.iterator(); try { @@ -323,7 +321,6 @@ public Object[] toArray() { } @Override - @SuppressWarnings("NullableProblems") public T[] toArray(T[] array) { Iterator iterator = AbstractModel.this.iterator(); try { @@ -371,7 +368,6 @@ public boolean addAll(Collection collection) { } @Override - @SuppressWarnings("NullableProblems") public boolean retainAll(Collection collection) { Iterator iterator = iterator(); try { @@ -431,7 +427,6 @@ public boolean contains(Object object) { } @Override - @SuppressWarnings("NullableProblems") public Object[] toArray() { Iterator iterator = iterator(); try { @@ -448,7 +443,6 @@ public Object[] toArray() { } @Override - @SuppressWarnings("NullableProblems") public T[] toArray(T[] array) { Iterator iterator = iterator(); try { @@ -523,7 +517,6 @@ public boolean addAll(Collection collection) { } @Override - @SuppressWarnings("NullableProblems") public boolean retainAll(Collection collection) { boolean modified = false; diff --git a/src/main/java/fr/inria/corese/core/next/data/spi/model/ReadOnlyModel.java b/src/main/java/fr/inria/corese/core/next/data/spi/model/ReadOnlyModel.java index 2d4a8ed86..236dcae3e 100644 --- a/src/main/java/fr/inria/corese/core/next/data/spi/model/ReadOnlyModel.java +++ b/src/main/java/fr/inria/corese/core/next/data/spi/model/ReadOnlyModel.java @@ -107,7 +107,6 @@ public Model filter(Resource subject, IRI predicate, Value object, Resource... c * Returns an unmodifiable iterator over the statements in the model. */ @Override - @SuppressWarnings("NullableProblems") public Iterator iterator() { return Collections.unmodifiableSet(delegate).iterator(); } diff --git a/src/main/java/fr/inria/corese/core/next/data/spi/term/literal/AbstractDuration.java b/src/main/java/fr/inria/corese/core/next/data/spi/term/literal/AbstractDuration.java index 7b3aa4cb1..10ff2fa7e 100644 --- a/src/main/java/fr/inria/corese/core/next/data/spi/term/literal/AbstractDuration.java +++ b/src/main/java/fr/inria/corese/core/next/data/spi/term/literal/AbstractDuration.java @@ -11,6 +11,7 @@ import fr.inria.corese.core.next.data.api.literal.CoreDatatype; import fr.inria.corese.core.next.data.api.literal.XSDDatatype; +import fr.inria.corese.core.next.data.api.term.IRI; /** * Abstract class representing a duration literal in RDF. @@ -21,7 +22,16 @@ public abstract class AbstractDuration extends AbstractLiteral implements Compar * Constructor for AbstractDuration. */ protected AbstractDuration() { - super(XSDDatatype.DURATION.getIRI()); + this(XSDDatatype.DURATION.getIRI()); + } + + /** + * Constructor for AbstractDuration with explicit datatype. + * + * @param datatype the datatype IRI, or null for default duration + */ + protected AbstractDuration(IRI datatype) { + super(datatype == null ? XSDDatatype.DURATION.getIRI() : datatype); } @Override diff --git a/src/main/java/fr/inria/corese/core/next/query/api/exception/QueryEvaluationException.java b/src/main/java/fr/inria/corese/core/next/query/api/exception/QueryEvaluationException.java index d168e387e..b52f1ebfc 100644 --- a/src/main/java/fr/inria/corese/core/next/query/api/exception/QueryEvaluationException.java +++ b/src/main/java/fr/inria/corese/core/next/query/api/exception/QueryEvaluationException.java @@ -3,6 +3,7 @@ /** * Thrown when a syntactically valid query fails during evaluation (execution). */ +@SuppressWarnings("java:S110") public class QueryEvaluationException extends QueryException { /** diff --git a/src/main/java/fr/inria/corese/core/next/query/api/exception/QuerySyntaxException.java b/src/main/java/fr/inria/corese/core/next/query/api/exception/QuerySyntaxException.java index f97a137db..0cc4162c6 100644 --- a/src/main/java/fr/inria/corese/core/next/query/api/exception/QuerySyntaxException.java +++ b/src/main/java/fr/inria/corese/core/next/query/api/exception/QuerySyntaxException.java @@ -3,6 +3,7 @@ /** * Thrown when a SPARQL query or update string contains invalid syntax. */ +@SuppressWarnings("java:S110") public class QuerySyntaxException extends QueryException { private final int line; diff --git a/src/main/java/fr/inria/corese/core/next/query/api/exception/QueryTimeoutException.java b/src/main/java/fr/inria/corese/core/next/query/api/exception/QueryTimeoutException.java index a9ad2cee0..d5d75c31a 100644 --- a/src/main/java/fr/inria/corese/core/next/query/api/exception/QueryTimeoutException.java +++ b/src/main/java/fr/inria/corese/core/next/query/api/exception/QueryTimeoutException.java @@ -3,6 +3,7 @@ /** * Thrown when a query evaluation exceeds its configured timeout limit. */ +@SuppressWarnings("java:S110") public class QueryTimeoutException extends QueryEvaluationException { /** diff --git a/src/main/java/fr/inria/corese/core/next/query/api/exception/RepositoryException.java b/src/main/java/fr/inria/corese/core/next/query/api/exception/RepositoryException.java index 2b5c73091..30296af26 100644 --- a/src/main/java/fr/inria/corese/core/next/query/api/exception/RepositoryException.java +++ b/src/main/java/fr/inria/corese/core/next/query/api/exception/RepositoryException.java @@ -7,6 +7,7 @@ * Thrown when an operation on a {@link Repository} * or {@link RepositoryConnection} fails. */ +@SuppressWarnings("java:S110") public class RepositoryException extends QueryException { /** diff --git a/src/main/java/fr/inria/corese/core/next/query/api/repository/MaterializedResults.java b/src/main/java/fr/inria/corese/core/next/query/api/repository/MaterializedResults.java index 45bedbd27..7f2cb8c9d 100644 --- a/src/main/java/fr/inria/corese/core/next/query/api/repository/MaterializedResults.java +++ b/src/main/java/fr/inria/corese/core/next/query/api/repository/MaterializedResults.java @@ -134,7 +134,6 @@ public Value getValue(String name) { } @Override - @SuppressWarnings("NullableProblems") public Iterator iterator() { return values.entrySet().stream() .map(entry -> new ImmutableBinding(entry.getKey(), entry.getValue())) diff --git a/src/main/java/fr/inria/corese/core/next/query/api/result/StatementResult.java b/src/main/java/fr/inria/corese/core/next/query/api/result/StatementResult.java index c2eeacf9f..43da14775 100644 --- a/src/main/java/fr/inria/corese/core/next/query/api/result/StatementResult.java +++ b/src/main/java/fr/inria/corese/core/next/query/api/result/StatementResult.java @@ -30,7 +30,6 @@ public interface StatementResult extends Closeable, Iterable { Statement next(); @Override - @SuppressWarnings("NullableProblems") default Iterator iterator() { return new Iterator<>() { @Override diff --git a/src/main/java/fr/inria/corese/core/next/query/api/result/TupleQueryResult.java b/src/main/java/fr/inria/corese/core/next/query/api/result/TupleQueryResult.java index 6b6cef4d6..e01f3012e 100644 --- a/src/main/java/fr/inria/corese/core/next/query/api/result/TupleQueryResult.java +++ b/src/main/java/fr/inria/corese/core/next/query/api/result/TupleQueryResult.java @@ -56,7 +56,6 @@ public interface TupleQueryResult extends Closeable, Iterable { * @return an iterator over the binding sets in this result */ @Override - @SuppressWarnings("NullableProblems") default Iterator iterator() { return new Iterator<>() { @Override diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/eval/ResultsImpl.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/eval/ResultsImpl.java index 0eef3ecbc..fa490c658 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/eval/ResultsImpl.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/engine/eval/ResultsImpl.java @@ -22,7 +22,6 @@ public List getSelect() { } @Override - @SuppressWarnings("NullableProblems") public Iterator iterator() { Iterator it = maps.iterator(); return new Iterator<>() { diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/event/KgramEventDispatcher.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/event/KgramEventDispatcher.java index aad20fe07..c3ab90590 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/event/KgramEventDispatcher.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/engine/event/KgramEventDispatcher.java @@ -17,7 +17,6 @@ public class KgramEventDispatcher implements Iterable { List observers = new ArrayList<>(); @Override - @SuppressWarnings("NullableProblems") public Iterator iterator() { return observers.iterator(); } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/path/PathMappingBuffer.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/path/PathMappingBuffer.java index 163e9bc46..c94694f9b 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/path/PathMappingBuffer.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/engine/path/PathMappingBuffer.java @@ -58,7 +58,6 @@ public synchronized void put(Mapping val, boolean next) { } @Override - @SuppressWarnings("NullableProblems") public Iterator iterator() { return this; } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/query/AbstractCoreseQuery.java b/src/main/java/fr/inria/corese/core/next/query/impl/query/AbstractCoreseQuery.java index f99104dc8..abc1dae48 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/query/AbstractCoreseQuery.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/query/AbstractCoreseQuery.java @@ -135,7 +135,6 @@ public Value getValue(String name) { } @Override - @SuppressWarnings("NullableProblems") public Iterator iterator() { return map.entrySet().stream() .map(e -> new CoreseBinding(e.getKey(), e.getValue())) diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/DatasetClauseAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/DatasetClauseAst.java index 385887ba6..9d9004b39 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/DatasetClauseAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/DatasetClauseAst.java @@ -20,11 +20,7 @@ public static DatasetClauseAst none() { @Override public void accept(AstVisitor visitor) { visitor.visit(this); - this.graphs.forEach(iriAst -> { - iriAst.accept(visitor); - }); - this.namedGraphs.forEach(iriAst -> { - iriAst.accept(visitor); - }); + this.graphs.forEach(iriAst -> iriAst.accept(visitor)); + this.namedGraphs.forEach(iriAst -> iriAst.accept(visitor)); } } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/DescribeQueryAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/DescribeQueryAst.java index cb6a5f872..efb7d12e2 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/DescribeQueryAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/DescribeQueryAst.java @@ -82,9 +82,7 @@ public void accept(AstVisitor visitor) { visitor.visit(this); this.prologue.accept(visitor); this.datasetClause.accept(visitor); - this.described.forEach(termAst -> { - termAst.accept(visitor); - }); + this.described.forEach(termAst -> termAst.accept(visitor)); this.whereClause.accept(visitor); this.valuesClause.accept(visitor); this.solutionModifier.accept(visitor); diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/PrefixDeclarationAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/PrefixDeclarationAst.java index ed29d202a..45e621475 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/PrefixDeclarationAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/PrefixDeclarationAst.java @@ -17,7 +17,7 @@ public record PrefixDeclarationAst(String prefix, IriAst namespace) implements V throw new IllegalArgumentException("prefix must be non-null"); } prefix = stripTrailingColon(prefix); - namespace = Objects.requireNonNull(namespace, "namespace"); + Objects.requireNonNull(namespace, "namespace"); if(! namespace.raw().isEmpty()) { namespace = new IriAst(stripAngleBrackets(namespace.raw())); } diff --git a/src/main/java/fr/inria/corese/core/next/storage/impl/model/StorageModel.java b/src/main/java/fr/inria/corese/core/next/storage/impl/model/StorageModel.java index d842077f7..877e264e8 100644 --- a/src/main/java/fr/inria/corese/core/next/storage/impl/model/StorageModel.java +++ b/src/main/java/fr/inria/corese/core/next/storage/impl/model/StorageModel.java @@ -208,7 +208,6 @@ public Model filter(Resource subject, IRI predicate, Value object, Resource... c return new FilteredModel(this, subject, predicate, object, contexts) { @Override - @SuppressWarnings("NullableProblems") public Iterator iterator() { return StorageModel.this.getFilterIterator(subject, predicate, object, contexts); } @@ -245,7 +244,6 @@ public Optional removeNamespace(String prefix) { } @Override - @SuppressWarnings("NullableProblems") public Iterator iterator() { return getFilterIterator(null, null, null); } diff --git a/src/test/java/fr/inria/corese/core/next/data/api/term/SimpleDateTest.java b/src/test/java/fr/inria/corese/core/next/data/api/term/SimpleDateTest.java index 977fb25d8..cbc4f24b8 100644 --- a/src/test/java/fr/inria/corese/core/next/data/api/term/SimpleDateTest.java +++ b/src/test/java/fr/inria/corese/core/next/data/api/term/SimpleDateTest.java @@ -15,7 +15,6 @@ import org.junit.jupiter.params.provider.ValueSource; import fr.inria.corese.core.next.data.api.exception.IncorrectOperationException; -import fr.inria.corese.core.next.data.api.literal.CoreDatatype; import fr.inria.corese.core.next.data.api.literal.XSDDatatype; class SimpleDateTest { @@ -50,13 +49,10 @@ void constructsFromCalendar() { @Test void handlesDatatypeConstructors() { assertEquals(new SimpleDate("2024-02-29Z"), new SimpleDate("2024-02-29Z", null)); - assertEquals(new SimpleDate("2024-02-29Z"), new SimpleDate("2024-02-29Z", null, null)); IRI custom = new SimpleIRI("urn:custom:temporal"); SimpleDate literal = new SimpleDate("2024-02-29Z", custom); assertEquals(custom, literal.getDatatype()); assertEquals(XSDDatatype.DATE, literal.getCoreDatatype()); - assertEquals(XSDDatatype.DATE, new SimpleDate("2024-02-29Z", custom, CoreDatatype.NONE).getCoreDatatype()); - assertEquals(custom, new SimpleDate("2024-02-29Z", custom, XSDDatatype.STRING).getDatatype()); } @Test @@ -114,7 +110,6 @@ void rejectsNullLexicalValues() { assertEquals("lexicalValue", assertThrows(NullPointerException.class, () -> new SimpleDate((String) null)).getMessage()); assertThrows(NullPointerException.class, () -> new SimpleDate(null, null)); - assertThrows(NullPointerException.class, () -> new SimpleDate(null, null, null)); } @ParameterizedTest diff --git a/src/test/java/fr/inria/corese/core/next/data/api/term/SimpleDateTimeTest.java b/src/test/java/fr/inria/corese/core/next/data/api/term/SimpleDateTimeTest.java index be1a74668..d2cc97081 100644 --- a/src/test/java/fr/inria/corese/core/next/data/api/term/SimpleDateTimeTest.java +++ b/src/test/java/fr/inria/corese/core/next/data/api/term/SimpleDateTimeTest.java @@ -15,7 +15,6 @@ import org.junit.jupiter.params.provider.ValueSource; import fr.inria.corese.core.next.data.api.exception.IncorrectOperationException; -import fr.inria.corese.core.next.data.api.literal.CoreDatatype; import fr.inria.corese.core.next.data.api.literal.XSDDatatype; class SimpleDateTimeTest { @@ -50,13 +49,10 @@ void constructsFromCalendar() { @Test void handlesDatatypeConstructors() { assertEquals(new SimpleDateTime("2024-02-29T12:30:45Z"), new SimpleDateTime("2024-02-29T12:30:45Z", null)); - assertEquals(new SimpleDateTime("2024-02-29T12:30:45Z"), new SimpleDateTime("2024-02-29T12:30:45Z", null, null)); IRI custom = new SimpleIRI("urn:custom:temporal"); SimpleDateTime literal = new SimpleDateTime("2024-02-29T12:30:45Z", custom); assertEquals(custom, literal.getDatatype()); assertEquals(XSDDatatype.DATETIME, literal.getCoreDatatype()); - assertEquals(XSDDatatype.DATETIME, new SimpleDateTime("2024-02-29T12:30:45Z", custom, CoreDatatype.NONE).getCoreDatatype()); - assertEquals(custom, new SimpleDateTime("2024-02-29T12:30:45Z", custom, XSDDatatype.STRING).getDatatype()); } @Test @@ -114,7 +110,6 @@ void rejectsNullLexicalValues() { assertEquals("lexicalValue", assertThrows(NullPointerException.class, () -> new SimpleDateTime((String) null)).getMessage()); assertThrows(NullPointerException.class, () -> new SimpleDateTime(null, null)); - assertThrows(NullPointerException.class, () -> new SimpleDateTime(null, null, null)); } @ParameterizedTest diff --git a/src/test/java/fr/inria/corese/core/next/data/api/term/SimpleDurationTest.java b/src/test/java/fr/inria/corese/core/next/data/api/term/SimpleDurationTest.java index 9009f7b76..07bd6e8f9 100644 --- a/src/test/java/fr/inria/corese/core/next/data/api/term/SimpleDurationTest.java +++ b/src/test/java/fr/inria/corese/core/next/data/api/term/SimpleDurationTest.java @@ -19,7 +19,6 @@ import org.junit.jupiter.params.provider.ValueSource; import fr.inria.corese.core.next.data.api.exception.IncorrectOperationException; -import fr.inria.corese.core.next.data.api.literal.CoreDatatype; import fr.inria.corese.core.next.data.api.literal.XSDDatatype; class SimpleDurationTest { @@ -69,15 +68,14 @@ void constructsFromTemporalAmounts() { } @Test - void retainsDurationDatatypeForAllConstructors() { + void handlesDatatypeConstructors() { SimpleDuration expected = new SimpleDuration("PT1S"); assertEquals(expected, new SimpleDuration("PT1S", null)); - assertEquals(expected, new SimpleDuration("PT1S", null, null)); assertEquals(expected, new SimpleDuration("PT1S", XSDDatatype.DURATION.getIRI())); - assertEquals(expected, new SimpleDuration("PT1S", new SimpleIRI("urn:custom:duration"))); - assertEquals(expected, new SimpleDuration("PT1S", XSDDatatype.STRING.getIRI(), CoreDatatype.NONE)); - assertEquals(XSDDatatype.DURATION, - new SimpleDuration("PT1S", null, XSDDatatype.STRING).getCoreDatatype()); + IRI custom = new SimpleIRI("urn:custom:duration"); + SimpleDuration literal = new SimpleDuration("PT1S", custom); + assertEquals(custom, literal.getDatatype()); + assertEquals(XSDDatatype.DURATION, literal.getCoreDatatype()); } @ParameterizedTest @@ -159,7 +157,6 @@ void rejectsNullLexicalValues() { assertEquals("lexicalValue", assertThrows(NullPointerException.class, () -> new SimpleDuration((String) null)).getMessage()); assertThrows(NullPointerException.class, () -> new SimpleDuration(null, null)); - assertThrows(NullPointerException.class, () -> new SimpleDuration(null, null, null)); } @ParameterizedTest diff --git a/src/test/java/fr/inria/corese/core/next/data/api/term/SimpleTimeTest.java b/src/test/java/fr/inria/corese/core/next/data/api/term/SimpleTimeTest.java index d40979151..d778dc130 100644 --- a/src/test/java/fr/inria/corese/core/next/data/api/term/SimpleTimeTest.java +++ b/src/test/java/fr/inria/corese/core/next/data/api/term/SimpleTimeTest.java @@ -15,7 +15,6 @@ import org.junit.jupiter.params.provider.ValueSource; import fr.inria.corese.core.next.data.api.exception.IncorrectOperationException; -import fr.inria.corese.core.next.data.api.literal.CoreDatatype; import fr.inria.corese.core.next.data.api.literal.XSDDatatype; class SimpleTimeTest { @@ -50,13 +49,10 @@ void constructsFromCalendar() { @Test void handlesDatatypeConstructors() { assertEquals(new SimpleTime("12:30:45Z"), new SimpleTime("12:30:45Z", null)); - assertEquals(new SimpleTime("12:30:45Z"), new SimpleTime("12:30:45Z", null, null)); IRI custom = new SimpleIRI("urn:custom:temporal"); SimpleTime literal = new SimpleTime("12:30:45Z", custom); assertEquals(custom, literal.getDatatype()); assertEquals(XSDDatatype.TIME, literal.getCoreDatatype()); - assertEquals(XSDDatatype.TIME, new SimpleTime("12:30:45Z", custom, CoreDatatype.NONE).getCoreDatatype()); - assertEquals(custom, new SimpleTime("12:30:45Z", custom, XSDDatatype.STRING).getDatatype()); } @Test @@ -116,7 +112,6 @@ void rejectsNullLexicalValues() { assertEquals("lexicalValue", assertThrows(NullPointerException.class, () -> new SimpleTime((String) null)).getMessage()); assertThrows(NullPointerException.class, () -> new SimpleTime(null, null)); - assertThrows(NullPointerException.class, () -> new SimpleTime(null, null, null)); } @ParameterizedTest diff --git a/tools/extract_sonar_alerts.py b/tools/extract_sonar_alerts.py new file mode 100755 index 000000000..d981cc758 --- /dev/null +++ b/tools/extract_sonar_alerts.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 +""" +Extract SonarLint alerts and diagnostics directly from the VS Code SonarLint storage. +Connects to the H2 database maintained by the SonarLint language server in VS Code. +""" + +import argparse +import json +import os +import subprocess +import sys +from collections import Counter +from pathlib import Path + +SONARLINT_JAR = Path.home() / ".vscode/extensions/sonarsource.sonarlint-vscode-5.9.1-linux-x64/server/sonarlint-ls.jar" +DB_URL = f"jdbc:h2:{Path.home()}/.sonarlint/storage/h2/sq-ide;AUTO_SERVER=TRUE" + + +def query_findings(prefix="src/main/java/fr/inria/corese/core/next"): + sql = f"""SELECT IDE_RELATIVE_FILE_PATH, RULE_KEY, START_LINE, MESSAGE, FINDING_TYPE + FROM KNOWN_FINDINGS + WHERE IDE_RELATIVE_FILE_PATH LIKE '{prefix}%' + ORDER BY IDE_RELATIVE_FILE_PATH, START_LINE;""" + + cmd = [ + "java", "-cp", str(SONARLINT_JAR), + "org.h2.tools.Shell", + "-url", DB_URL, + "-user", "sa", "-password", "", + "-sql", sql + ] + res = subprocess.run(cmd, capture_output=True, text=True, check=True) + + findings = [] + for line in res.stdout.splitlines(): + parts = [p.strip() for p in line.split("|")] + if len(parts) >= 5 and parts[0].startswith(prefix): + rel_path = parts[0] + if os.path.exists(rel_path): + findings.append({ + "file": rel_path, + "rule": parts[1], + "line": int(parts[2]) if parts[2].isdigit() else 0, + "message": parts[3], + "type": parts[4] + }) + return findings + + +def main(): + parser = argparse.ArgumentParser(description="Extract active SonarLint alerts from VS Code H2 database.") + parser.add_argument("--path", default="src/main/java/fr/inria/corese/core/next", help="Path prefix to filter files") + parser.add_argument("--json", dest="json_out", help="Path to write JSON output") + parser.add_argument("--summary", action="store_true", help="Print summary by rule and by file") + args = parser.parse_args() + + findings = query_findings(args.path) + + files_map = {} + rule_counter = Counter() + for f in findings: + files_map.setdefault(f["file"], []).append(f) + rule_counter[f["rule"]] += 1 + + print(f"SonarLint Analysis Report for: {args.path}") + print(f"Total findings on active files: {len(findings)} across {len(files_map)} files") + + if args.summary or not args.json_out: + print("\n--- Summary by Rule ---") + for rule, count in rule_counter.most_common(): + print(f" {rule:12s}: {count:3d}") + + print("\n--- Files with Most Findings ---") + sorted_files = sorted(files_map.items(), key=lambda kv: len(kv[1]), reverse=True) + for filepath, file_findings in sorted_files[:25]: + print(f" {len(file_findings):3d} issues: {filepath}") + + if args.json_out: + out_data = { + "total_findings": len(findings), + "files_count": len(files_map), + "by_rule": dict(rule_counter), + "findings": findings + } + Path(args.json_out).write_text(json.dumps(out_data, indent=2), encoding="utf-8") + print(f"\nWrote full JSON findings to {args.json_out}") + + +if __name__ == "__main__": + main() diff --git a/tools/sonarlint_extract.py b/tools/sonarlint_extract.py new file mode 100644 index 000000000..089f40484 --- /dev/null +++ b/tools/sonarlint_extract.py @@ -0,0 +1,557 @@ +#!/usr/bin/env python3 +""" +Extract SonarLint diagnostics for Java files by driving the SonarLint +Language Server (the same `sonarlint-ls.jar` bundled with the VS Code +SonarLint extension) directly over stdio, using the Language Server +Protocol (LSP) - no VS Code UI required. + +How it works +------------ +The VS Code SonarLint extension does not do the Java analysis itself: +it starts `sonarlint-ls.jar` (a standalone LSP server, class +`org.sonarsource.sonarlint.ls.ServerMain`) as a child process and talks +to it over stdio using standard LSP JSON-RPC messages. The extension's +Java support is provided by the `sonarjava.jar` analyzer plugin that +ships in the extension's `analyzers/` folder and is passed to the +server on the command line. + +We can reproduce exactly that handshake ourselves: + + 1. Launch: + java -jar sonarlint-ls.jar -stdio -analyzers ... + (`-stdio` tells it to use stdio transport instead of a TCP port; + `-analyzers` is a space separated list of analyzer plugin jars, + confirmed by inspecting the jar's picocli `@Option` annotations in + `org.sonarsource.sonarlint.ls.ServerMain`.) + + 2. Send the LSP `initialize` request. SonarLint expects some + SonarLint-specific keys inside `initializationOptions` + (`productKey`, `productName`, `productVersion`, `workspaceName`, + `disableTelemetry`, ... - discovered the same way, by inspecting + the strings/constant pool of the server's compiled classes). + + 3. Send `initialized` notification. + + 4. For each Java file, send `textDocument/didOpen` with the file + content. SonarLint analyzes the file asynchronously and reports + results back to us as `textDocument/publishDiagnostics` + notifications (standard LSP) - we just listen for those. + + 5. Collect all `publishDiagnostics` notifications, wait for the + server to go quiet (or hit a timeout), then shut down cleanly + with `shutdown` + `exit`. + +This script implements a minimal, dependency-free LSP client +(Content-Length framed JSON-RPC over the subprocess's stdin/stdout) +sufficient to do exactly this. + +Usage +----- + python3 sonarlint_extract.py \ + --src-root src/main/java/fr/inria/corese/core/next \ + --output sonarlint_report.json + +Requirements +------------ + - Java (the JRE bundled inside the VS Code extension is reused by + default; override with --java to point elsewhere). + - sonarlint-ls.jar and analyzers/sonarjava.jar from the SonarLint + VS Code extension (paths below have working defaults, override + with --sonarlint-ls / --analyzer-jar if your extension version + or install location differs). +""" + +from __future__ import annotations + +import argparse +import json +import os +import subprocess +import sys +import threading +import time +from pathlib import Path +from typing import Any, Dict, List, Optional + +EXT_DIR = Path.home() / ".vscode/extensions/sonarsource.sonarlint-vscode-5.9.1-linux-x64" +DEFAULT_SONARLINT_LS = EXT_DIR / "server/sonarlint-ls.jar" +DEFAULT_ANALYZER_JARS = [ + EXT_DIR / "analyzers/sonarjava.jar", + EXT_DIR / "analyzers/sonarjavasymbolicexecution.jar", +] +DEFAULT_JAVA = EXT_DIR / "jre/21.0.12.1-linux-x86_64.tar/bin/java" + +# How long (seconds) to wait for diagnostics after opening the LAST file +# before we assume analysis has settled and it's safe to shut down. +DEFAULT_QUIET_PERIOD = 8.0 +# Hard cap on total analysis wait time, in case something never quiets down. +DEFAULT_MAX_WAIT = 600.0 + + +class LspClient: + """A minimal LSP client that speaks Content-Length-framed JSON-RPC + over a subprocess's stdin/stdout, similar to what VS Code itself + does when talking to sonarlint-ls.""" + + def __init__(self, proc: subprocess.Popen, java_config: Optional[dict] = None): + self.proc = proc + self.java_config = java_config + self._id = 0 + self._lock = threading.Lock() + self._pending: Dict[int, threading.Event] = {} + self._results: Dict[int, Any] = {} + self.diagnostics: Dict[str, List[dict]] = {} + self._diag_lock = threading.Lock() + self._last_diagnostic_time = time.time() + self._activity_lock = threading.Lock() + self.last_activity_time = time.time() + self._stop_reader = False + self._reader_thread = threading.Thread(target=self._read_loop, daemon=True) + self._reader_thread.start() + self._stderr_thread = threading.Thread(target=self._drain_stderr, daemon=True) + self._stderr_thread.start() + + # ---- low level framing ------------------------------------------------- + def _write(self, payload: dict) -> None: + body = json.dumps(payload).encode("utf-8") + header = f"Content-Length: {len(body)}\r\n\r\n".encode("ascii") + assert self.proc.stdin is not None + self.proc.stdin.write(header + body) + self.proc.stdin.flush() + + def _drain_stderr(self) -> None: + assert self.proc.stderr is not None + debug = os.environ.get("SONARLINT_DEBUG") + for line in self.proc.stderr: + if debug: + sys.stderr.write("[sonarlint-ls] " + line.decode(errors="replace")) + + def _read_loop(self) -> None: + stdout = self.proc.stdout + assert stdout is not None + while not self._stop_reader: + line = stdout.readline() + if not line: + break + if line.strip() == b"": + continue + if line.lower().startswith(b"content-length:"): + length = int(line.split(b":", 1)[1].strip()) + # consume remaining headers until blank line + while True: + hline = stdout.readline() + if hline in (b"\r\n", b"\n", b""): + break + body = stdout.read(length) + try: + msg = json.loads(body.decode("utf-8")) + except json.JSONDecodeError: + continue + self._handle_message(msg) + + def _handle_message(self, msg: dict) -> None: + # Any message received from the server (log, diagnostics, progress, + # requests, responses, ...) counts as activity, so the quiet-period + # detection in the main loop can rely on a single, reliable signal + # instead of guessing from specific message contents. + with self._activity_lock: + self.last_activity_time = time.time() + + if "id" in msg and ("result" in msg or "error" in msg): + # response to one of our requests + msg_id = msg["id"] + with self._lock: + ev = self._pending.get(msg_id) + self._results[msg_id] = msg + if ev: + ev.set() + return + + method = msg.get("method") + if method == "textDocument/publishDiagnostics": + params = msg.get("params", {}) + uri = params.get("uri", "") + diags = params.get("diagnostics", []) + with self._diag_lock: + self.diagnostics[uri] = diags + self._last_diagnostic_time = time.time() + sys.stderr.write(f"[diagnostics] {uri}: {len(diags)} issues\n") + elif method and msg.get("id") is not None: + req_id = msg["id"] + if method == "workspace/configuration": + items = msg.get("params", {}).get("items", []) + self._write({"jsonrpc": "2.0", "id": req_id, "result": [{}] * len(items)}) + elif method == "sonarlint/isOpenInEditor": + self._write({"jsonrpc": "2.0", "id": req_id, "result": True}) + elif method == "sonarlint/listFilesInFolder": + self._write({"jsonrpc": "2.0", "id": req_id, "result": {"foundFiles": []}}) + elif method == "sonarlint/filterOutExcludedFiles": + params = msg.get("params", {}) + uris = params.get("fileUris", []) if isinstance(params, dict) else params + self._write({"jsonrpc": "2.0", "id": req_id, "result": {"fileUris": uris}}) + elif method == "sonarlint/hasJoinedIdeLabs": + self._write({"jsonrpc": "2.0", "id": req_id, "result": False}) + elif method == "sonarlint/getJavaConfig": + self._write({"jsonrpc": "2.0", "id": req_id, "result": self.java_config}) + else: + self._write({"jsonrpc": "2.0", "id": req_id, "result": None}) + elif method == "window/logMessage": + msg_text = msg.get("params", {}).get("message", "") + sys.stderr.write(f"[log] {msg_text}\n") + # Note: we no longer treat a single "Analysis detected" (or similar) + # log line as a reliable "analysis complete" signal - SonarLint can + # emit several of these across batches. Completion is instead + # decided by the quiet-period check on `last_activity_time` in the + # main loop, which is updated above for every message received. + # Otherwise: ignore other notifications (progress, telemetry, etc.) - + # `last_activity_time` was already refreshed above regardless of type. + + # ---- JSON-RPC helpers --------------------------------------------------- + def request(self, method: str, params: Optional[dict] = None, timeout: float = 30.0) -> dict: + with self._lock: + self._id += 1 + msg_id = self._id + ev = threading.Event() + self._pending[msg_id] = ev + self._write({"jsonrpc": "2.0", "id": msg_id, "method": method, "params": params or {}}) + if not ev.wait(timeout): + raise TimeoutError(f"Timed out waiting for response to {method!r}") + with self._lock: + result = self._results.pop(msg_id) + self._pending.pop(msg_id, None) + if "error" in result: + raise RuntimeError(f"{method} failed: {result['error']}") + return result.get("result") + + def notify(self, method: str, params: Optional[dict] = None) -> None: + self._write({"jsonrpc": "2.0", "method": method, "params": params or {}}) + + def seconds_since_last_diagnostic(self) -> float: + with self._diag_lock: + return time.time() - self._last_diagnostic_time + + def close(self) -> None: + self._stop_reader = True + + +def start_server(java: Path, sonarlint_ls: Path, analyzer_jars: List[Path], work_dir: Path) -> subprocess.Popen: + cmd = [str(java), "-jar", str(sonarlint_ls), "-stdio", "-analyzers"] + [str(j) for j in analyzer_jars] + return subprocess.Popen( + cmd, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + cwd=str(work_dir), + ) + + +def to_file_uri(path: Path) -> str: + return path.resolve().as_uri() + + +def load_java_config(project_root: Path) -> Optional[Dict[str, Any]]: + """Build the `sonarlint/getJavaConfig` response from the compile + classpath resolved by the `sonarResolver` Gradle task. + + Gradle writes a JSON document (despite the plain `properties` + filename) to `build/sonar-resolver/properties` containing a + `compileClasspath` list. If that file doesn't exist yet, run + `./gradlew sonarResolver` to generate it. Returns None (and lets + the server fall back to its own project detection) if the + classpath still can't be determined. + """ + resolver_file = project_root / "build" / "sonar-resolver" / "properties" + if not resolver_file.exists(): + print(f"{resolver_file} not found; running './gradlew sonarResolver' to generate it...", + file=sys.stderr) + try: + subprocess.run( + ["./gradlew", "sonarResolver"], + cwd=str(project_root), + check=True, + ) + except (OSError, subprocess.CalledProcessError) as e: + print(f"warning: failed to run './gradlew sonarResolver': {e}", file=sys.stderr) + return None + + if not resolver_file.exists(): + print(f"warning: {resolver_file} still not found after running sonarResolver", + file=sys.stderr) + return None + + try: + data = json.loads(resolver_file.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError) as e: + print(f"warning: could not parse {resolver_file}: {e}", file=sys.stderr) + return None + + compile_classpath = data.get("compileClasspath") or [] + classes_dir = project_root / "build" / "classes" / "java" / "main" + classpath = [str(classes_dir)] + list(compile_classpath) + + return { + "projectRoot": str(project_root), + "sourceLevel": "21", + "classpath": classpath, + "isTest": False, + "vmLocation": "", + } + + +def main() -> int: + parser = argparse.ArgumentParser(description="Extract SonarLint diagnostics for Java files via the SonarLint Language Server (LSP over stdio).") + parser.add_argument("--src-root", default="src/main/java/fr/inria/corese/core/next", + help="Directory tree to scan for *.java files (default: %(default)s)") + parser.add_argument("--project-root", default=".", + help="Project/workspace root passed to SonarLint as the workspace folder (default: current directory)") + parser.add_argument("--files", nargs="*", default=None, + help="Specific .java files to scan instead of an entire tree") + parser.add_argument("--output", default="sonarlint_report.json", help="Where to write the JSON report") + parser.add_argument("--sonarlint-ls", default=str(DEFAULT_SONARLINT_LS), help="Path to sonarlint-ls.jar") + parser.add_argument("--analyzer-jar", action="append", default=None, + help="Analyzer jar to load (repeatable). Defaults to sonarjava.jar + symbolic execution plugin.") + parser.add_argument("--java", default=str(DEFAULT_JAVA), help="Path to java executable") + parser.add_argument("--quiet-period", type=float, default=DEFAULT_QUIET_PERIOD, + help="Seconds of no new diagnostics before considering analysis complete (default: %(default)s)") + parser.add_argument("--max-wait", type=float, default=DEFAULT_MAX_WAIT, + help="Absolute cap in seconds on total time spent waiting for diagnostics") + parser.add_argument("--batch-size", type=int, default=25, + help="Number of files to open concurrently per batch (avoids opening 700 files at once)") + args = parser.parse_args() + + project_root = Path(args.project_root).resolve() + src_root = Path(args.src_root) + if not src_root.is_absolute(): + src_root = project_root / src_root + if not src_root.is_dir(): + print(f"error: src root not found: {src_root}", file=sys.stderr) + return 1 + + java = Path(args.java) + if not java.exists(): + java = Path("java") # fall back to whatever is on PATH + + sonarlint_ls = Path(args.sonarlint_ls) + if not sonarlint_ls.exists(): + print(f"error: sonarlint-ls.jar not found at {sonarlint_ls}", file=sys.stderr) + return 1 + + analyzer_jars = [Path(p) for p in (args.analyzer_jar or DEFAULT_ANALYZER_JARS)] + for jar in analyzer_jars: + if not jar.exists(): + print(f"error: analyzer jar not found: {jar}", file=sys.stderr) + return 1 + + if args.files: + java_files = [Path(f).resolve() for f in args.files] + else: + java_files = sorted(src_root.rglob("*.java")) + if not java_files: + print(f"error: no .java files found", file=sys.stderr) + return 1 + print(f"Found {len(java_files)} Java files to analyze") + + java_config = load_java_config(project_root) + + extra_properties: Dict[str, str] = {} + if java_config and java_config.get("classpath"): + extra_properties["sonar.java.binaries"] = str(project_root / "build" / "classes" / "java" / "main") + + print(f"Starting sonarlint-ls: {java} -jar {sonarlint_ls} -stdio -analyzers " + f"{' '.join(str(j) for j in analyzer_jars)}") + proc = start_server(java, sonarlint_ls, analyzer_jars, project_root) + client = LspClient(proc, java_config=java_config) + + try: + init_result = client.request("initialize", { + "processId": os.getpid(), + "rootUri": to_file_uri(project_root), + "capabilities": { + "textDocument": { + "publishDiagnostics": { + "relatedInformation": True, + "versionSupport": False, + "tagSupport": {"valueSet": [1, 2]}, + "codeDescriptionSupport": True, + "dataSupport": True, + } + }, + "workspace": { + "configuration": True, + } + }, + "workspaceFolders": [ + {"uri": to_file_uri(project_root), "name": project_root.name} + ], + "initializationOptions": { + "productKey": "standalone", + "productName": "SonarLint CLI Extractor", + "productVersion": "1.0.0", + "workspaceName": project_root.name, + "firstSecretDetected": True, + "showVerboseLogs": True, + "additionalAttributes": {}, + "disableTelemetry": True, + "automaticAnalysis": True, + "connections": {"sonarqube": [], "sonarcloud": []}, + "rules": {}, + "extraProperties": extra_properties, + }, + }, timeout=60) + print("Server initialized:", + init_result.get("serverInfo") if isinstance(init_result, dict) else init_result) + client.notify("initialized", {}) + + # Give the server a moment to finish standalone-engine bootstrap + # (it lazily starts the Java analysis engine on first use). + time.sleep(1.0) + + opened_uris: List[str] = [] + batch_size = max(1, args.batch_size) + for start in range(0, len(java_files), batch_size): + batch = java_files[start:start + batch_size] + for f in batch: + uri = to_file_uri(f) + try: + text = f.read_text(encoding="utf-8", errors="replace") + except OSError as e: + print(f"warning: could not read {f}: {e}", file=sys.stderr) + continue + client.notify("textDocument/didOpen", { + "textDocument": { + "uri": uri, + "languageId": "java", + "version": 1, + "text": text, + } + }) + opened_uris.append(uri) + print(f"Opened {min(start + batch_size, len(java_files))}/{len(java_files)} files...") + # Small pause between batches so we don't overwhelm the analyzer queue. + time.sleep(0.2) + + print("All files opened. Waiting for SonarLint analysis to settle " + f"(quiet period: {args.quiet_period}s, max wait: {args.max_wait}s)...") + wait_start = time.time() + last_progress_print = 0.0 + progress_interval = 5.0 + while True: + now = time.time() + with client._activity_lock: + idle_for = now - client.last_activity_time + elapsed = now - wait_start + + if now - last_progress_print >= progress_interval: + files_with_diagnostics = len(client.diagnostics) + print(f" ...{elapsed:.0f}s elapsed, {files_with_diagnostics}/{len(opened_uris)} " + f"files have received diagnostics, idle for {idle_for:.1f}s") + last_progress_print = now + + files_with_diagnostics = len(client.diagnostics) + required_quiet = args.quiet_period if files_with_diagnostics >= len(opened_uris) else max(args.quiet_period, 15.0) + if idle_for >= required_quiet: + print(f"No server activity for {idle_for:.1f}s (>= required quiet period of " + f"{required_quiet:.1f}s). Assuming analysis has settled.") + break + if elapsed >= args.max_wait: + print(f"Reached max wait of {args.max_wait}s. Proceeding with whatever " + "diagnostics have been received so far.") + break + time.sleep(0.5) + + # Some files may never receive a publishDiagnostics message if + # SonarLint found zero issues in them (LSP servers are not + # required to publish an empty diagnostics list). Fill those + # in explicitly so the report always reflects every scanned file. + for uri in opened_uris: + client.diagnostics.setdefault(uri, []) + + report = build_report(client.diagnostics, project_root) + Path(args.output).write_text(json.dumps(report, indent=2), encoding="utf-8") + print(f"Wrote report with {report['summary']['total_issues']} issue(s) " + f"across {report['summary']['files_with_issues']} file(s) to {args.output}") + print_summary(report) + return 0 + finally: + try: + client.request("shutdown", {}, timeout=10) + client.notify("exit", {}) + except Exception: + pass + client.close() + try: + proc.wait(timeout=10) + except Exception: + proc.kill() + + +def build_report(diagnostics: Dict[str, List[dict]], project_root: Path) -> dict: + files = [] + total_issues = 0 + files_with_issues = 0 + severity_counts: Dict[str, int] = {} + rule_counts: Dict[str, int] = {} + + for uri, diags in sorted(diagnostics.items()): + rel_path = uri + if uri.startswith("file://"): + abs_path = Path(uri[len("file://"):]) + try: + rel_path = str(abs_path.relative_to(project_root)) + except ValueError: + rel_path = str(abs_path) + + issues = [] + for d in diags: + severity = d.get("severity") + code = d.get("code") + rule = code if isinstance(code, str) else (code.get("value") if isinstance(code, dict) else code) + rng = d.get("range", {}) + issues.append({ + "rule": rule, + "message": d.get("message"), + "severity": severity, + "start_line": rng.get("start", {}).get("line", 0) + 1, + "start_character": rng.get("start", {}).get("character", 0), + "end_line": rng.get("end", {}).get("line", 0) + 1, + "end_character": rng.get("end", {}).get("character", 0), + "source": d.get("source"), + }) + total_issues += 1 + if rule: + rule_counts[str(rule)] = rule_counts.get(str(rule), 0) + 1 + severity_counts[str(severity)] = severity_counts.get(str(severity), 0) + 1 + + if issues: + files_with_issues += 1 + files.append({"file": rel_path, "issue_count": len(issues), "issues": issues}) + + return { + "summary": { + "total_files_scanned": len(files), + "files_with_issues": files_with_issues, + "total_issues": total_issues, + "by_severity": severity_counts, + "by_rule": dict(sorted(rule_counts.items(), key=lambda kv: -kv[1])), + }, + "files": files, + } + + +def print_summary(report: dict) -> None: + top_rules = list(report["summary"]["by_rule"].items())[:15] + if top_rules: + print("\nTop rules triggered:") + for rule, count in top_rules: + print(f" {count:4d} {rule}") + top_files = sorted(report["files"], key=lambda f: -f["issue_count"])[:15] + if top_files and top_files[0]["issue_count"] > 0: + print("\nFiles with the most issues:") + for f in top_files: + if f["issue_count"] == 0: + break + print(f" {f['issue_count']:4d} {f['file']}") + + +if __name__ == "__main__": + raise SystemExit(main())