Skip to content

Commit 90a9f06

Browse files
committed
🐛 fix(SHACL-core): improve SHACL violation parsing with better error handling
1 parent 4b45368 commit 90a9f06

2 files changed

Lines changed: 66 additions & 6 deletions

File tree

rocrate_validator/requirements/shacl/checks.py

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -196,12 +196,33 @@ def __do_execute_check__(self, shacl_context: SHACLValidationContext):
196196
logger.debug("Parsing Validation with result: %s", shacl_result)
197197
# process the failed checks to extract the requirement checks involved
198198
for violation in shacl_result.violations:
199-
shape = shapes_registry.get_shape(Shape.compute_key(shapes_graph, violation.sourceShape))
200-
assert shape is not None, "Unable to map the violation to a shape"
199+
shape = None
200+
try:
201+
shape = shapes_registry.get_shape(Shape.compute_key(shapes_graph, violation.sourceShape))
202+
except (ValueError, KeyError):
203+
# sourceShape may be a BNode (e.g. an inline sh:sparql constraint).
204+
# Attempt to resolve the owning NodeShape/PropertyShape via the graph.
205+
shape = resolve_parent_shape(shapes_graph, violation.sourceShape, shapes_registry)
206+
if shape is None:
207+
logger.warning(
208+
"Unable to map violation to a shape (sourceShape: %s); skipping",
209+
violation.sourceShape,
210+
)
211+
continue
212+
if shape is None:
213+
logger.warning(
214+
"Shape not found for violation (sourceShape: %s)",
215+
violation.sourceShape,
216+
)
217+
continue
201218
requirementCheck = SHACLCheck.get_instance(shape)
202-
assert requirementCheck is not None, "The requirement check cannot be None"
203-
if (not shacl_context.settings.skip_checks or
204-
requirementCheck.identifier not in shacl_context.settings.skip_checks):
219+
if requirementCheck is None:
220+
logger.warning("No check instance found for shape: %s", shape.key)
221+
continue
222+
if (
223+
not shacl_context.settings.skip_checks
224+
or requirementCheck.identifier not in shacl_context.settings.skip_checks
225+
):
205226
failed_requirements_checks.add(requirementCheck)
206227
violations = failed_requirements_checks_violations.get(requirementCheck.identifier, None)
207228
if violations is None:

rocrate_validator/requirements/shacl/utils.py

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
import hashlib
1818
from pathlib import Path
19-
from typing import Optional, Union
19+
from typing import TYPE_CHECKING, Optional, Union
2020

2121
from rdflib import RDF, BNode, Graph, Namespace
2222
from rdflib.term import Node
@@ -26,6 +26,9 @@
2626
from rocrate_validator.errors import BadSyntaxError
2727
from rocrate_validator.models import Severity
2828

29+
if TYPE_CHECKING:
30+
from rocrate_validator.requirements.shacl.models import Shape
31+
2932
# set up logging
3033
logger = logging.getLogger(__name__)
3134

@@ -296,3 +299,39 @@ def load_shapes_from_graph(g: Graph) -> ShapesList:
296299
subgraphs[shape] = subgraph
297300

298301
return ShapesList(node_shapes, property_shapes, subgraphs, g)
302+
303+
304+
def resolve_parent_shape(
305+
shapes_graph: Graph, source_shape_node: Node, shapes_registry
306+
) -> Optional[Shape]:
307+
"""
308+
Try to resolve the parent NodeShape/PropertyShape for a BNode constraint node.
309+
310+
When a SPARQL constraint (sh:sparql) or inline constraint BNode fires a violation,
311+
pyshacl reports the BNode as `sh:sourceShape`. That BNode is not registered
312+
in the ShapesRegistry directly; instead the containing NodeShape is.
313+
This helper walks up via sh:sparql and sh:property predicates to find it.
314+
"""
315+
from rocrate_validator.requirements.shacl.models import Shape
316+
317+
if not isinstance(source_shape_node, BNode):
318+
return None
319+
SHACL = Namespace(SHACL_NS)
320+
# Predicates via which a NodeShape/PropertyShape can own a constraint BNode
321+
parent_predicates = [SHACL.sparql, SHACL.property]
322+
for predicate in parent_predicates:
323+
for parent_node in shapes_graph.subjects(predicate, source_shape_node):
324+
try:
325+
parent_shape = shapes_registry.get_shape(
326+
Shape.compute_key(shapes_graph, parent_node)
327+
)
328+
if parent_shape is not None:
329+
logger.debug(
330+
"Resolved parent shape %s for SPARQL/inline constraint BNode %s",
331+
parent_shape.key,
332+
source_shape_node,
333+
)
334+
return parent_shape
335+
except (ValueError, KeyError):
336+
continue
337+
return None

0 commit comments

Comments
 (0)