Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,21 @@ public static Location locateWithHint(
return toLineColumn(query, best);
}

/**
* Locates the first occurrence of a SPARQL variable in the query text, trying both the {@code
* ?name} and {@code $name} spellings. Returns {@link #UNKNOWN} when the variable cannot be found.
*/
public static Location locateVariable(String query, String name) {
if (query == null || name == null || name.isEmpty()) {
return UNKNOWN;
}
var offsets = new ArrayList<Integer>();
collectAllWholeToken(query, "?" + name, offsets);
collectAllWholeToken(query, "$" + name, offsets);
offsets.sort(Integer::compare);
return offsets.isEmpty() ? UNKNOWN : toLineColumn(query, offsets.get(0));
}

// ---- offset collection -----------------------------------------------------------------

private static List<Integer> findAllTermOffsets(String query, Node term, PrefixMapping prefixes) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ public SparqlValidationResult validate(
a.dynamicClass());
List<SparqlValidationAnnotation> ann =
validateReferences(refs, scope, a.query().getPrefixMapping(), originalText);
ann.addAll(UnusedVariableCheck.check(a.query(), originalText));
return new SparqlValidationResult(originalText, plan, ann);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,8 @@ public SparqlValidationResult validateSparql(
}

/**
* Performs a schema-independent syntax check, returning only {@code SYNTAX_ERROR} annotations.
* Performs a schema-independent check: syntax, plus — for inputs that parse as a query — the
* (equally schema-independent) unused-variable analysis of {@link UnusedVariableCheck}.
*
* <p>Used as a fallback when no schema can be resolved (for example a {@code # [endpoint=...]} is
* briefly unreachable) so that a cell containing broken SPARQL still gets squiggles rather than
Expand All @@ -165,17 +166,25 @@ public SparqlValidationResult validateSparql(
* {@code cim:} and friends without an explicit {@code PREFIX} line is not mis-reported as a
* syntax error.
*
* @return a result whose annotations are empty when the input parses as either a query or an
* update, or hold a single {@code SYNTAX_ERROR} when neither parser accepts it
* @return a result whose annotations are empty when the input parses cleanly, hold {@code
* PROJECTED_VARIABLE_UNBOUND}/{@code UNUSED_VARIABLE} warnings for a query with unused
* variables, or hold a single {@code SYNTAX_ERROR} when neither parser accepts the input
*/
public static SparqlValidationResult checkSyntaxOnly(String input) {
Objects.requireNonNull(input, "input");
DefaultPrefixes.InjectionResult inj = DefaultPrefixes.inject(input, DefaultPrefixes.BUILT_IN);
SparqlQueryAnalyzer analyzer = new SparqlQueryAnalyzer();
InvalidQueryException queryError;
try {
analyzer.parse(inj.text());
return new SparqlValidationResult(input, null, List.of());
Query query = analyzer.parse(inj.text());
var warnings = UnusedVariableCheck.check(query, inj.text());
if (warnings.isEmpty()) {
return new SparqlValidationResult(input, null, List.of());
}
SparqlValidationResult raw = new SparqlValidationResult(inj.text(), null, warnings);
return inj.injectedLineCount() > 0
? adjustLineNumbers(raw, input, inj.injectedLineCount())
: new SparqlValidationResult(input, null, warnings);
} catch (InvalidQueryException e) {
queryError = e; // remember the query error; it is more informative than the update one
}
Expand Down Expand Up @@ -238,9 +247,11 @@ public static ShaclValidationResult checkShaclSyntaxOnly(
Graph shapesGraph, boolean checkStandardVocabulary) {
Objects.requireNonNull(shapesGraph, "shapesGraph");
var extractor = new ShaclSparqlExtractor();
Set<String> exemptVars = shaclExemptVariableNames(shapesGraph);
var embeddedResults = new ArrayList<ShaclEmbeddedQueryResult>();
for (EmbeddedSparql q : extractor.extract(shapesGraph)) {
SparqlValidationResult r = checkSyntaxOnly(q.renderedQuery());
SparqlValidationResult r =
suppressShaclPreboundVariables(checkSyntaxOnly(q.renderedQuery()), exemptVars);
embeddedResults.add(new ShaclEmbeddedQueryResult(q, r));
}
// The vocabulary-typo check is schema-independent (it consults only the bundled W3C
Expand Down Expand Up @@ -769,20 +780,70 @@ private ShaclValidationResult validateShacl(Graph shapesGraph, ValidationScope s
// Embedded-SPARQL analysis: sh:select, sh:ask, sh:construct.
// For each query, pass $this → sh:targetClass as a subject-type hint so that
// domain/range checks fire correctly even when $this has no explicit rdf:type in the query.
Set<String> exemptVars = shaclExemptVariableNames(shapesGraph);
var embeddedResults = new ArrayList<ShaclEmbeddedQueryResult>();
for (EmbeddedSparql q : shaclExtractor.extract(shapesGraph)) {
Map<Node, Set<Node>> hints =
q.targetClasses().isEmpty()
? Map.of()
: Map.of(org.apache.jena.sparql.core.Var.alloc("this"), q.targetClasses());
var r = validator.validate(renderedQueryWithPath(q), scope, hints);
r = suppressImpliedType(r);
r = suppressShaclPreboundVariables(suppressImpliedType(r), exemptVars);
embeddedResults.add(new ShaclEmbeddedQueryResult(q, r));
}

return new ShaclValidationResult(shapeAnnotations, embeddedResults);
}

/**
* Variables the SHACL engine pre-binds in embedded SPARQL constraints and validators ({@code
* $this}, {@code ?value}, …), plus the {@code $PATH} placeholder. They are legitimately projected
* or referenced without being bound in the query body, so the unused-variable warnings must not
* fire for them.
*/
private static final Set<String> SHACL_PREBOUND_VARIABLES =
Set.of("this", "value", "PATH", "shapesGraph", "currentShape");

/**
* Returns the full set of variable names the unused-variable check must treat as pre-bound for
* {@code shapesGraph}: the fixed {@link #SHACL_PREBOUND_VARIABLES} plus every custom {@code
* sh:ConstraintComponent} parameter name declared in the graph itself (see {@link
* ShaclSparqlExtractor#collectConstraintParameterNames(Graph)}). Custom parameters are just as
* pre-bound as {@code $this} — a component declaring {@code sh:parameter [ sh:path ex:myList ]}
* legitimately references {@code $myList} in its SPARQL body without binding it there.
*/
private static Set<String> shaclExemptVariableNames(Graph shapesGraph) {
var names = new LinkedHashSet<>(SHACL_PREBOUND_VARIABLES);
names.addAll(ShaclSparqlExtractor.collectConstraintParameterNames(shapesGraph));
return names;
}

/**
* Removes {@link SparqlValidationCode#PROJECTED_VARIABLE_UNBOUND} / {@link
* SparqlValidationCode#UNUSED_VARIABLE} annotations about SHACL pre-bound variables from an
* embedded-SPARQL result — e.g. {@code SELECT $this WHERE { FILTER NOT EXISTS { ... } }} is a
* perfectly valid constraint because the engine binds {@code $this} before evaluation.
*
* @param exemptNames the pre-bound variable names for the enclosing shapes graph, from {@link
* #shaclExemptVariableNames(Graph)}
*/
private static SparqlValidationResult suppressShaclPreboundVariables(
SparqlValidationResult r, Set<String> exemptNames) {
var filtered =
r.annotations().stream()
.filter(
a ->
!((a.code() == SparqlValidationCode.PROJECTED_VARIABLE_UNBOUND
|| a.code() == SparqlValidationCode.UNUSED_VARIABLE)
&& a.term() != null
&& a.term().isVariable()
&& exemptNames.contains(a.term().getName())))
.toList();
return filtered.size() == r.annotations().size()
? r
: new SparqlValidationResult(r.query(), r.queryPlan(), filtered);
}

/**
* Removes {@link SparqlValidationCode#QUERY_IMPLIED_TYPE} annotations from an embedded-SPARQL
* result before it is stored in the SHACL validation report.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,5 +78,17 @@ public enum SparqlValidationCode {
* CIM {@code cims:multiplicity} — e.g. requiring more values than the schema's upper bound
* allows, so the constraint can never be satisfied against conformant data.
*/
CARDINALITY_INCOMPATIBLE_WITH_MULTIPLICITY
CARDINALITY_INCOMPATIBLE_WITH_MULTIPLICITY,
/**
* A variable in the result surface (explicit {@code SELECT} list, {@code CONSTRUCT} template,
* {@code DESCRIBE} list) that appears nowhere in the query body ({@code WHERE}/{@code
* BIND}/{@code VALUES}) — it is unbound in every result, almost always a typo (e.g. {@code ?nmae}
* vs {@code ?name}).
*/
PROJECTED_VARIABLE_UNBOUND,
/**
* A variable bound in a triple pattern, {@code BIND} or {@code VALUES} that is never projected,
* filtered on, ordered/grouped by, or otherwise reused — a typo or a leftover from a query edit.
*/
UNUSED_VARIABLE
}
Loading