diff --git a/.changeset/many-queens-matter.md b/.changeset/many-queens-matter.md new file mode 100644 index 0000000..090f262 --- /dev/null +++ b/.changeset/many-queens-matter.md @@ -0,0 +1,5 @@ +--- +"rdf-validate-shacl": patch +--- + +Added a `maxNodeChecks` option to prevent `too much recursion` error caused by cyclic shape references (fixes #136) diff --git a/src/validation-engine.js b/src/validation-engine.js index b4bbfbc..49627d2 100644 --- a/src/validation-engine.js +++ b/src/validation-engine.js @@ -4,12 +4,14 @@ import ValidationReport from './validation-report.js' import { extractStructure, extractSourceShapeStructure } from './dataset-utils.js' const error = debug('validation-enging::error') +const defaultMaxNodeChecks = 50 class ValidationEngine { constructor(context, options) { this.context = context this.factory = context.factory this.maxErrors = options.maxErrors + this.maxNodeChecks = options.maxNodeChecks === undefined ? defaultMaxNodeChecks : options.maxNodeChecks this.initReport() this.recordErrorsLevel = 0 this.violationsCount = 0 @@ -18,11 +20,12 @@ class ValidationEngine { } clone() { - return new ValidationEngine(this.context, { maxErrors: this.maxErrors }) + return new ValidationEngine(this.context, { maxErrors: this.maxErrors, maxNodeChecks: this.maxNodeChecks }) } initReport() { const { rdf, sh } = this.context.ns + this.nodeCheckCounters = {} this.reportPointer = clownface({ dataset: this.factory.dataset(), @@ -67,6 +70,18 @@ class ValidationEngine { if (shape.deactivated) return false + if (this.maxNodeChecks > 0) { + // check how many times we have already tested this focusNode against this shape + const id = JSON.stringify([focusNode, shape.shapeNode]) + const nodeCheckCounter = this.nodeCheckCounters[id] === undefined ? 0 : this.nodeCheckCounters[id] + if (nodeCheckCounter > this.maxNodeChecks) { + // max node checks reached, so bail out + return false + } + // increment check counter for given focusNode/shape pair + this.nodeCheckCounters[id] = nodeCheckCounter + 1 + } + const valueNodes = shape.getValueNodes(focusNode, dataGraph) let errorFound = false for (const constraint of shape.constraints) { diff --git a/test/data/data-shapes/custom/circularReferences.ttl b/test/data/data-shapes/custom/circularReferences.ttl new file mode 100644 index 0000000..9139467 --- /dev/null +++ b/test/data/data-shapes/custom/circularReferences.ttl @@ -0,0 +1,48 @@ +@prefix dash: . +@prefix ex: . +@prefix mf: . +@prefix owl: . +@prefix rdf: . +@prefix rdfs: . +@prefix sh: . +@prefix sht: . +@prefix xsd: . + +<> + rdf:type mf:Manifest ; + mf:entries ( + + ) ; +. + + + rdf:type sht:Validate ; + rdfs:label "Test of circular references in data graph" ; + mf:action [ + sht:dataGraph <> ; + sht:shapesGraph <> ; + ] ; + mf:result [ + rdf:type sh:ValidationReport ; + sh:conforms "true"^^xsd:boolean ; + ] ; +. + +ex:Thing + a sh:NodeShape,rdfs:Class ; + sh:property [ + sh:path ex:references ; + sh:node ex:Thing + ] ; + sh:targetClass +. + + + a ; + ex:references +. + + + a ; + ex:references ; +. diff --git a/test/data/data-shapes/custom/manifest.ttl b/test/data/data-shapes/custom/manifest.ttl index 4ca60dd..53a4352 100644 --- a/test/data/data-shapes/custom/manifest.ttl +++ b/test/data/data-shapes/custom/manifest.ttl @@ -11,4 +11,5 @@ mf:include ; mf:include ; mf:include ; + mf:include ; .