From 0192d4ad50161b50ee83b8d7b03e05c369da56f8 Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Fri, 23 Oct 2026 16:34:47 +0200 Subject: [PATCH 1/6] chore(bundler): fix some test fixtures Signed-off-by: Jan Kowalleck --- tools/src/main/js/bundler/src/sanity.js | 15 +++++++-- .../js/bundler/test/fixtures/main.schema.json | 12 ------- .../modules/used-array-items.schema.json | 21 ------------ .../snapshot/main-bundled.min.schema.json | 2 +- .../test/snapshot/main-bundled.schema.json | 33 ------------------- .../main/js/bundler/test/snapshot/report.json | 8 ----- 6 files changed, 14 insertions(+), 77 deletions(-) diff --git a/tools/src/main/js/bundler/src/sanity.js b/tools/src/main/js/bundler/src/sanity.js index a6959da14..82bb745d4 100644 --- a/tools/src/main/js/bundler/src/sanity.js +++ b/tools/src/main/js/bundler/src/sanity.js @@ -1,5 +1,5 @@ import {objectGetOwnPath, PATH_NOT_FOUND} from "./helpers/common.js"; -import {REF_FRAG_SEP, REF_KEYWORDS, subschemas} from './helpers/json-schema-spec.js'; +import {isSchemaObject, REF_FRAG_SEP, REF_KEYWORDS, subschemas} from './helpers/json-schema-spec.js'; import {isJsonPointer, jsonPointer2stack} from './helpers/json-pointer-spec.js' /** @typedef {import('./helpers/json-schema-spec.js').Schema} Schema */ @@ -33,11 +33,15 @@ const validateRefs = (schema) => { /** @type {Set} */ const localRefs = new Set(); + /** @type {WeakSet} */ + const _seen = new WeakSet(); /** * @param {Readonly} schema * @returns {void} */ const gatherRefs = (schema) => { + if (_seen.has(schema)) return; + _seen.add(schema); for (const k of REF_KEYWORDS) { const v = schema[k]; if (typeof v !== 'string') continue; @@ -54,9 +58,16 @@ const validateRefs = (schema) => { for (const ref of localRefs) { if (isJsonPointer(ref)) { - if (objectGetOwnPath(schema, jsonPointer2stack(ref)) === PATH_NOT_FOUND) { + const rTarget = objectGetOwnPath(schema, jsonPointer2stack(ref)); + if (rTarget === PATH_NOT_FOUND) { errors.add(`Missing local ref: ${REF_FRAG_SEP}${ref}`); } + if (isSchemaObject(rTarget)) { + gatherRefs(rTarget); + } else if (typeof rTarget !== 'boolean') { + // ref must resolve to a schema or boolean; + throw new RangeError(`Illegal local ref target: ${REF_FRAG_SEP}${ref}`); + } } else { warnings.add(`Unchecked local ref: ${REF_FRAG_SEP}${ref}`); } diff --git a/tools/src/main/js/bundler/test/fixtures/main.schema.json b/tools/src/main/js/bundler/test/fixtures/main.schema.json index 1dd5b3291..ff59a103a 100644 --- a/tools/src/main/js/bundler/test/fixtures/main.schema.json +++ b/tools/src/main/js/bundler/test/fixtures/main.schema.json @@ -81,18 +81,6 @@ "description": "The last alternative of the module's choice.", "$ref": "modules/used-array-items.schema.json#/properties/choice/oneOf/1" }, - "statusValues": { - "$comment": "Ref to a NON-schema value (the module's enum array). Semantically dubious, but a legal pointer -> the tree-shaker must record it as reached (no crash) and keep the enum intact.", - "title": "Status values", - "description": "Pointer to the module's status enum.", - "$ref": "modules/used-array-items.schema.json#/properties/status/enum" - }, - "midLevel": { - "$comment": "Pointer to ONE ITEM of a NON-schema array (enum/1) of a schema that is itself not reached. The item must stay at index 1; its siblings may become void placeholders, like in schema arrays.", - "title": "Mid level", - "description": "The middle level literal.", - "$ref": "modules/used-array-items.schema.json#/properties/level/enum/1" - }, "anyValue": { "$comment": "Ref to an EMPTY schema `{}`. Reached -> must survive the sweep even though it has no keys at all.", "title": "Any value", diff --git a/tools/src/main/js/bundler/test/fixtures/modules/used-array-items.schema.json b/tools/src/main/js/bundler/test/fixtures/modules/used-array-items.schema.json index 35fe3e155..a07590bed 100644 --- a/tools/src/main/js/bundler/test/fixtures/modules/used-array-items.schema.json +++ b/tools/src/main/js/bundler/test/fixtures/modules/used-array-items.schema.json @@ -80,27 +80,6 @@ } ], "examples": [null, 1, 2, 3] - }, - "status": { - "$comment": "A NON-schema value of this schema (the enum array) is referenced from main via #/properties/status/enum. The enum must be kept as a whole; 'status' itself is not reached -> hollowed; its 'type' is dropped.", - "title": "Status", - "description": "A status value.", - "type": "string", - "enum": [ - "on", - "off" - ] - }, - "level": { - "$comment": "A single ITEM of a data array (enum/1) is referenced from main. Legal pointer, semantically dubious. 'level' itself is not reached -> hollowed, so the enum has no validation meaning anymore; enum/0 and enum/2 may become void placeholders so that the index of enum/1 stays stable. 'type' is dropped.", - "title": "Level", - "description": "A level value.", - "type": "string", - "enum": [ - "low", - "mid", - "high" - ] } }, "$defs": { diff --git a/tools/src/main/js/bundler/test/snapshot/main-bundled.min.schema.json b/tools/src/main/js/bundler/test/snapshot/main-bundled.min.schema.json index d6c542334..fbf6b2eee 100644 --- a/tools/src/main/js/bundler/test/snapshot/main-bundled.min.schema.json +++ b/tools/src/main/js/bundler/test/snapshot/main-bundled.min.schema.json @@ -1 +1 @@ -{"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"http://example.com/test/main-bundled.min.schema.json","$comment":"FIXTURE: root schema; entry point of the bundle. This $comment is the only one that survives minification. The root title/description must survive in both outputs.","title":"Main","description":"Root document of the bundler test fixtures.","type":"object","properties":{"$comment":{"type":"string","title":"Comment"},"whole":{"title":"Whole","description":"A module referenced in its entirety.","$ref":"#/$defs/used-whole"},"sub":{"title":"Sub","description":"A single definition of another module.","$ref":"#/$defs/used-subdefs-only/$defs/kept"},"local":{"title":"Local","description":"A definition of this very schema.","$ref":"#/$defs/main/$defs/mainLocal"},"composed":{"title":"Composed","description":"An object assembled from parts of another module.","type":"object","$defs":{"rootNestedDropped":{"title":"Root nested dropped","description":"An unreferenced nested definition of the root.","type":"null"}},"properties":{"street":{"title":"Street","description":"The street, taken from the module's address.","$ref":"#/$defs/used-properties-only/properties/address/properties/street"},"latitude":{"title":"Latitude","description":"The latitude, taken from the module's address.geo.","$ref":"#/$defs/used-properties-only/properties/address/properties/geo/properties/lat"},"contact":{"title":"Contact","description":"The contact block of the module, as is.","$ref":"#/$defs/used-properties-only/properties/contact"},"nested":{"title":"Nested","description":"A value defined in a property-level $defs.","$ref":"#/$defs/used-whole/properties/withDefs/$defs/nestedKept"}}},"middle":{"title":"Middle","description":"The middle part of the module's combined schema.","$ref":"#/$defs/used-array-items/properties/combined/allOf/1"},"lastChoice":{"title":"Last choice","description":"The last alternative of the module's choice.","$ref":"#/$defs/used-array-items/properties/choice/oneOf/1"},"statusValues":{"title":"Status values","description":"Pointer to the module's status enum.","$ref":"#/$defs/used-array-items/properties/status/enum"},"midLevel":{"title":"Mid level","description":"The middle level literal.","$ref":"#/$defs/used-array-items/properties/level/enum/1"},"anyValue":{"title":"Any value","description":"Accepts anything.","$ref":"#/$defs/used-subdefs-only/$defs/keptEmpty"},"todoValue":{"title":"Todo value","description":"A placeholder.","$ref":"#/$defs/used-subdefs-only/$defs/keptCommentOnly"},"annotatedValue":{"title":"Annotated value","description":"An annotation-only schema.","$ref":"#/$defs/used-subdefs-only/$defs/keptAnnotationsOnly"}},"$defs":{"dynamic-anchor":{"$dynamicAnchor":"node","title":"Dynamic anchor","description":"A recursive tree node, addressable via a dynamic anchor.","type":"object","properties":{"children":{"title":"Children","description":"The child nodes of this node.","type":"array","items":{"title":"Child","description":"A nested node.","$ref":"#/$defs/dynamic-anchor"}}}},"main":{"$defs":{"mainLocal":{"title":"Main local","description":"A local string definition of the root schema.","type":"string"},"mainUnused":{"title":"Main unused","description":"A definition nothing refers to.","type":"null"}},"not":{}},"unused":{"title":"Unused","description":"A module that nothing reachable refers to.","type":"object","properties":{"pullsNothingIn":{"title":"Pulls nothing in","description":"A flag defined by another module.","$ref":"#/$defs/used-whole/$defs/unused"}}},"used-array-items":{"title":"Used array items","description":"A module of which only individual array items and a non-schema value are referenced.","type":"object","properties":{"combined":{"title":"Combined","description":"A composition of which only the middle part is referenced.","allOf":[{"title":"Combined part 0","description":"The first part.","type":"object","properties":{"a":{"title":"A","description":"An unreferenced value.","type":"string"}}},{"title":"Combined part 1","description":"The middle part.","type":"object","properties":{"b":{"title":"B","description":"A value defined in this module.","$ref":"#/$defs/used-array-items/$defs/keptViaArray"}}},{"title":"Combined part 2","description":"The last part.","$ref":"#/$defs/unused"}]},"choice":{"title":"Choice","description":"A choice of which only the last alternative is referenced.","oneOf":[{"title":"Choice 0","description":"The first alternative.","type":"null","$defs":{"droppedDef":{"title":"Dropped def","description":"A definition nothing reachable refers to.","type":"null"}}},{"title":"Choice 1","description":"The second alternative.","type":"integer","$defs":{"droppedDef":{"title":"Dropped def","description":"A definition nothing reachable refers to.","type":"null"}}}],"examples":[null,1,2,3]},"status":{"title":"Status","description":"A status value.","type":"string","enum":["on","off"]},"level":{"title":"Level","description":"A level value.","type":"string","enum":["low","mid","high"]}},"$defs":{"keptViaArray":{"title":"Kept via array","description":"A boolean value.","type":"boolean"},"droppedDef":{"title":"Dropped def","description":"A definition nothing reachable refers to.","type":"null"}}},"used-properties-only":{"title":"Used properties only","description":"A module of which only nested properties are referenced.","type":"object","properties":{"address":{"title":"Address","description":"A postal address.","type":"object","properties":{"street":{"title":"Street","description":"Street name and number.","type":"string"},"city":{"title":"City","description":"Name of the city.","type":"string"},"geo":{"title":"Geo","description":"Geographic coordinates.","type":"object","properties":{"lat":{"title":"Latitude","description":"Latitude in degrees.","$ref":"#/$defs/used-properties-only/$defs/degrees"},"lon":{"title":"Longitude","description":"Longitude in degrees.","$ref":"#/$defs/used-properties-only/$defs/degrees"}}}}},"contact":{"title":"Contact","description":"Contact details.","type":"object","properties":{"email":{"title":"E-mail","description":"An e-mail address.","type":"string","format":"email"},"phone":{"title":"Phone","description":"A phone number.","type":"string"}}},"unusedTopLevel":{"title":"Unused top level","description":"A property nothing refers to.","$ref":"#/$defs/unused"}},"$defs":{"degrees":{"title":"Degrees","description":"An angle in degrees.","type":"number","minimum":-180,"maximum":180},"unusedDef":{"title":"Unused def","description":"A definition nothing reachable refers to.","type":"null"}}},"used-subdefs-only":{"title":"Used sub-defs only","description":"A module of which only individual definitions are referenced.","type":"object","properties":{"body":{"title":"Body","description":"The body of this module.","$ref":"#/$defs/unused"}},"$defs":{"kept":{"title":"Kept","description":"A definition that is reached directly from the root schema.","type":"object","properties":{"shared":{"title":"Shared","description":"A value defined by another module.","$ref":"#/$defs/used-whole/$defs/sharedByOther"},"fromMain":{"title":"From main","description":"A value defined by the root schema.","$ref":"#/$defs/main/$defs/mainLocal"},"chain":{"title":"Chain","description":"A value defined further down in this module.","$ref":"#/$defs/used-subdefs-only/$defs/keptTransitively"}}},"keptEmpty":{},"keptCommentOnly":{},"keptAnnotationsOnly":{"title":"Kept annotations only","description":"A schema that is just an annotation."},"keptTransitively":{"title":"Kept transitively","description":"A string value.","type":"string"},"dropped":{"title":"Dropped","description":"A definition nothing reachable refers to.","type":"null"}}},"used-whole":{"title":"Used whole","description":"A module that is referenced in its entirety.","type":"object","properties":{"name":{"title":"Name","description":"A display name.","type":"string","$defs":{"nestedDropped":{"title":"Nested dropped","description":"An unreferenced nested definition.","type":"null"}}},"withDefs":{"title":"With defs","description":"A property that hosts its own definitions.","anyOf":[{"title":"Is string","type":"string"},{"title":"Non-zero number","type":"number","if":{"const":0,"$defs":{"unused":{"title":"Unused","type":"null"}}},"then":false,"else":true,"$defs":{"unusedPositive":{"title":"Number > 0","type":"number","exclusiveMinimum":0},"unusedNegative":{"title":"Number < 0","type":"number","exclusiveMaximum":0}}}],"$defs":{"nestedKept":{"title":"Nested kept","description":"A referenced nested definition.","type":"integer"},"nestedDropped":{"title":"Nested dropped","description":"An unreferenced nested definition.","type":"null"}}},"inner":{"title":"Inner","description":"Sibling annotations next to a $ref: title/description are kept, while the sibling $comment is removed in the minified output.","$ref":"#/$defs/used-whole/$defs/inner"},"license":{"title":"License","description":"The license of this item.","$ref":"externals/not-bundled.schema.json#/$defs/licenseId"},"remote":{"title":"Remote","description":"A thing defined by a remote schema.","$ref":"https://example.com/remote.schema.json#/$defs/thing"},"inArray":{"title":"In array","description":"A composition of several referenced schemas.","allOf":[{"$ref":"#/$defs/used-whole/$defs/inner"},{"$ref":"#/$defs/used-subdefs-only/$defs/keptTransitively"},{"$ref":"externals/not-bundled.schema.json#/$defs/licenseId"}]}},"$defs":{"inner":{"title":"Inner","description":"An integer value.","type":"integer"},"unused":{"title":"Unused","description":"A flag.","type":"boolean"},"sharedByOther":{"title":"Shared by other","description":"A numeric value shared across modules.","type":"number"}}}}} \ No newline at end of file +{"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"http://example.com/test/main-bundled.min.schema.json","$comment":"FIXTURE: root schema; entry point of the bundle. This $comment is the only one that survives minification. The root title/description must survive in both outputs.","title":"Main","description":"Root document of the bundler test fixtures.","type":"object","properties":{"$comment":{"type":"string","title":"Comment"},"whole":{"title":"Whole","description":"A module referenced in its entirety.","$ref":"#/$defs/used-whole"},"sub":{"title":"Sub","description":"A single definition of another module.","$ref":"#/$defs/used-subdefs-only/$defs/kept"},"local":{"title":"Local","description":"A definition of this very schema.","$ref":"#/$defs/main/$defs/mainLocal"},"composed":{"title":"Composed","description":"An object assembled from parts of another module.","type":"object","$defs":{"rootNestedDropped":{"title":"Root nested dropped","description":"An unreferenced nested definition of the root.","type":"null"}},"properties":{"street":{"title":"Street","description":"The street, taken from the module's address.","$ref":"#/$defs/used-properties-only/properties/address/properties/street"},"latitude":{"title":"Latitude","description":"The latitude, taken from the module's address.geo.","$ref":"#/$defs/used-properties-only/properties/address/properties/geo/properties/lat"},"contact":{"title":"Contact","description":"The contact block of the module, as is.","$ref":"#/$defs/used-properties-only/properties/contact"},"nested":{"title":"Nested","description":"A value defined in a property-level $defs.","$ref":"#/$defs/used-whole/properties/withDefs/$defs/nestedKept"}}},"middle":{"title":"Middle","description":"The middle part of the module's combined schema.","$ref":"#/$defs/used-array-items/properties/combined/allOf/1"},"lastChoice":{"title":"Last choice","description":"The last alternative of the module's choice.","$ref":"#/$defs/used-array-items/properties/choice/oneOf/1"},"anyValue":{"title":"Any value","description":"Accepts anything.","$ref":"#/$defs/used-subdefs-only/$defs/keptEmpty"},"todoValue":{"title":"Todo value","description":"A placeholder.","$ref":"#/$defs/used-subdefs-only/$defs/keptCommentOnly"},"annotatedValue":{"title":"Annotated value","description":"An annotation-only schema.","$ref":"#/$defs/used-subdefs-only/$defs/keptAnnotationsOnly"}},"$defs":{"dynamic-anchor":{"$dynamicAnchor":"node","title":"Dynamic anchor","description":"A recursive tree node, addressable via a dynamic anchor.","type":"object","properties":{"children":{"title":"Children","description":"The child nodes of this node.","type":"array","items":{"title":"Child","description":"A nested node.","$ref":"#/$defs/dynamic-anchor"}}}},"main":{"$defs":{"mainLocal":{"title":"Main local","description":"A local string definition of the root schema.","type":"string"},"mainUnused":{"title":"Main unused","description":"A definition nothing refers to.","type":"null"}},"not":{}},"unused":{"title":"Unused","description":"A module that nothing reachable refers to.","type":"object","properties":{"pullsNothingIn":{"title":"Pulls nothing in","description":"A flag defined by another module.","$ref":"#/$defs/used-whole/$defs/unused"}}},"used-array-items":{"title":"Used array items","description":"A module of which only individual array items and a non-schema value are referenced.","type":"object","properties":{"combined":{"title":"Combined","description":"A composition of which only the middle part is referenced.","allOf":[{"title":"Combined part 0","description":"The first part.","type":"object","properties":{"a":{"title":"A","description":"An unreferenced value.","type":"string"}}},{"title":"Combined part 1","description":"The middle part.","type":"object","properties":{"b":{"title":"B","description":"A value defined in this module.","$ref":"#/$defs/used-array-items/$defs/keptViaArray"}}},{"title":"Combined part 2","description":"The last part.","$ref":"#/$defs/unused"}]},"choice":{"title":"Choice","description":"A choice of which only the last alternative is referenced.","oneOf":[{"title":"Choice 0","description":"The first alternative.","type":"null","$defs":{"droppedDef":{"title":"Dropped def","description":"A definition nothing reachable refers to.","type":"null"}}},{"title":"Choice 1","description":"The second alternative.","type":"integer","$defs":{"droppedDef":{"title":"Dropped def","description":"A definition nothing reachable refers to.","type":"null"}}}],"examples":[null,1,2,3]}},"$defs":{"keptViaArray":{"title":"Kept via array","description":"A boolean value.","type":"boolean"},"droppedDef":{"title":"Dropped def","description":"A definition nothing reachable refers to.","type":"null"}}},"used-properties-only":{"title":"Used properties only","description":"A module of which only nested properties are referenced.","type":"object","properties":{"address":{"title":"Address","description":"A postal address.","type":"object","properties":{"street":{"title":"Street","description":"Street name and number.","type":"string"},"city":{"title":"City","description":"Name of the city.","type":"string"},"geo":{"title":"Geo","description":"Geographic coordinates.","type":"object","properties":{"lat":{"title":"Latitude","description":"Latitude in degrees.","$ref":"#/$defs/used-properties-only/$defs/degrees"},"lon":{"title":"Longitude","description":"Longitude in degrees.","$ref":"#/$defs/used-properties-only/$defs/degrees"}}}}},"contact":{"title":"Contact","description":"Contact details.","type":"object","properties":{"email":{"title":"E-mail","description":"An e-mail address.","type":"string","format":"email"},"phone":{"title":"Phone","description":"A phone number.","type":"string"}}},"unusedTopLevel":{"title":"Unused top level","description":"A property nothing refers to.","$ref":"#/$defs/unused"}},"$defs":{"degrees":{"title":"Degrees","description":"An angle in degrees.","type":"number","minimum":-180,"maximum":180},"unusedDef":{"title":"Unused def","description":"A definition nothing reachable refers to.","type":"null"}}},"used-subdefs-only":{"title":"Used sub-defs only","description":"A module of which only individual definitions are referenced.","type":"object","properties":{"body":{"title":"Body","description":"The body of this module.","$ref":"#/$defs/unused"}},"$defs":{"kept":{"title":"Kept","description":"A definition that is reached directly from the root schema.","type":"object","properties":{"shared":{"title":"Shared","description":"A value defined by another module.","$ref":"#/$defs/used-whole/$defs/sharedByOther"},"fromMain":{"title":"From main","description":"A value defined by the root schema.","$ref":"#/$defs/main/$defs/mainLocal"},"chain":{"title":"Chain","description":"A value defined further down in this module.","$ref":"#/$defs/used-subdefs-only/$defs/keptTransitively"}}},"keptEmpty":{},"keptCommentOnly":{},"keptAnnotationsOnly":{"title":"Kept annotations only","description":"A schema that is just an annotation."},"keptTransitively":{"title":"Kept transitively","description":"A string value.","type":"string"},"dropped":{"title":"Dropped","description":"A definition nothing reachable refers to.","type":"null"}}},"used-whole":{"title":"Used whole","description":"A module that is referenced in its entirety.","type":"object","properties":{"name":{"title":"Name","description":"A display name.","type":"string","$defs":{"nestedDropped":{"title":"Nested dropped","description":"An unreferenced nested definition.","type":"null"}}},"withDefs":{"title":"With defs","description":"A property that hosts its own definitions.","anyOf":[{"title":"Is string","type":"string"},{"title":"Non-zero number","type":"number","if":{"const":0,"$defs":{"unused":{"title":"Unused","type":"null"}}},"then":false,"else":true,"$defs":{"unusedPositive":{"title":"Number > 0","type":"number","exclusiveMinimum":0},"unusedNegative":{"title":"Number < 0","type":"number","exclusiveMaximum":0}}}],"$defs":{"nestedKept":{"title":"Nested kept","description":"A referenced nested definition.","type":"integer"},"nestedDropped":{"title":"Nested dropped","description":"An unreferenced nested definition.","type":"null"}}},"inner":{"title":"Inner","description":"Sibling annotations next to a $ref: title/description are kept, while the sibling $comment is removed in the minified output.","$ref":"#/$defs/used-whole/$defs/inner"},"license":{"title":"License","description":"The license of this item.","$ref":"externals/not-bundled.schema.json#/$defs/licenseId"},"remote":{"title":"Remote","description":"A thing defined by a remote schema.","$ref":"https://example.com/remote.schema.json#/$defs/thing"},"inArray":{"title":"In array","description":"A composition of several referenced schemas.","allOf":[{"$ref":"#/$defs/used-whole/$defs/inner"},{"$ref":"#/$defs/used-subdefs-only/$defs/keptTransitively"},{"$ref":"externals/not-bundled.schema.json#/$defs/licenseId"}]}},"$defs":{"inner":{"title":"Inner","description":"An integer value.","type":"integer"},"unused":{"title":"Unused","description":"A flag.","type":"boolean"},"sharedByOther":{"title":"Shared by other","description":"A numeric value shared across modules.","type":"number"}}}}} \ No newline at end of file diff --git a/tools/src/main/js/bundler/test/snapshot/main-bundled.schema.json b/tools/src/main/js/bundler/test/snapshot/main-bundled.schema.json index 9b0426fdc..b683a4d17 100644 --- a/tools/src/main/js/bundler/test/snapshot/main-bundled.schema.json +++ b/tools/src/main/js/bundler/test/snapshot/main-bundled.schema.json @@ -81,18 +81,6 @@ "description": "The last alternative of the module's choice.", "$ref": "#/$defs/used-array-items/properties/choice/oneOf/1" }, - "statusValues": { - "$comment": "Ref to a NON-schema value (the module's enum array). Semantically dubious, but a legal pointer -> the tree-shaker must record it as reached (no crash) and keep the enum intact.", - "title": "Status values", - "description": "Pointer to the module's status enum.", - "$ref": "#/$defs/used-array-items/properties/status/enum" - }, - "midLevel": { - "$comment": "Pointer to ONE ITEM of a NON-schema array (enum/1) of a schema that is itself not reached. The item must stay at index 1; its siblings may become void placeholders, like in schema arrays.", - "title": "Mid level", - "description": "The middle level literal.", - "$ref": "#/$defs/used-array-items/properties/level/enum/1" - }, "anyValue": { "$comment": "Ref to an EMPTY schema `{}`. Reached -> must survive the sweep even though it has no keys at all.", "title": "Any value", @@ -248,27 +236,6 @@ 2, 3 ] - }, - "status": { - "$comment": "A NON-schema value of this schema (the enum array) is referenced from main via #/properties/status/enum. The enum must be kept as a whole; 'status' itself is not reached -> hollowed; its 'type' is dropped.", - "title": "Status", - "description": "A status value.", - "type": "string", - "enum": [ - "on", - "off" - ] - }, - "level": { - "$comment": "A single ITEM of a data array (enum/1) is referenced from main. Legal pointer, semantically dubious. 'level' itself is not reached -> hollowed, so the enum has no validation meaning anymore; enum/0 and enum/2 may become void placeholders so that the index of enum/1 stays stable. 'type' is dropped.", - "title": "Level", - "description": "A level value.", - "type": "string", - "enum": [ - "low", - "mid", - "high" - ] } }, "$defs": { diff --git a/tools/src/main/js/bundler/test/snapshot/report.json b/tools/src/main/js/bundler/test/snapshot/report.json index 51ed1efe0..7afd047ad 100644 --- a/tools/src/main/js/bundler/test/snapshot/report.json +++ b/tools/src/main/js/bundler/test/snapshot/report.json @@ -71,14 +71,6 @@ "modules/used-array-items.schema.json#/properties/choice/oneOf/1", "#/$defs/used-array-items/properties/choice/oneOf/1" ], - [ - "modules/used-array-items.schema.json#/properties/status/enum", - "#/$defs/used-array-items/properties/status/enum" - ], - [ - "modules/used-array-items.schema.json#/properties/level/enum/1", - "#/$defs/used-array-items/properties/level/enum/1" - ], [ "modules/used-subdefs-only.schema.json#/$defs/keptEmpty", "#/$defs/used-subdefs-only/$defs/keptEmpty" From ddddc6607afc664163eeff532b65eb61dc3b56ce Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Fri, 23 Oct 2026 17:24:23 +0200 Subject: [PATCH 2/6] chore(bundler): refactor Signed-off-by: Jan Kowalleck --- tools/src/main/js/bundler/src/bundle.js | 35 +++++++++++++------ .../bundler/src/helpers/json-schema-spec.js | 24 +++++++++++-- tools/src/main/js/bundler/src/sanity.js | 19 ++++++---- 3 files changed, 59 insertions(+), 19 deletions(-) diff --git a/tools/src/main/js/bundler/src/bundle.js b/tools/src/main/js/bundler/src/bundle.js index d2af811d0..b1142593d 100644 --- a/tools/src/main/js/bundler/src/bundle.js +++ b/tools/src/main/js/bundler/src/bundle.js @@ -9,7 +9,7 @@ import { import { ID_KEYWORD, NOT_KEYWORD, SCHEMA_KEYWORD, COMMENT_KEYWORD, DEFS_KEYWORD, REF_KEYWORDS, isSchemaObject, subschemas, - refIsAbsolute, refSplit, refJoin, REF_FRAG_SEP + refIsAbsolute, refSplit, refJoin, makeLocalRef } from './helpers/json-schema-spec.js'; import {PATH_SEP as URI_PATH_SEP} from './helpers/uri.js'; @@ -19,7 +19,9 @@ const HOLLOW_COMMENT = 'Itself must not be used for validation.'; -const FRAG_DEFS_PREFIX = `${JP_PREFIX}${DEFS_KEYWORD}${JP_PREFIX}`; +const JP_DEFS = `${JP_PREFIX}${DEFS_KEYWORD}`; +const JP_DEFS_PREFIX = `${JP_DEFS}${JP_PREFIX}`; + /** * @param {string} f @@ -50,13 +52,17 @@ class ModuleNameCollisionError extends Error { } } +/** @typedef {import('./helpers/json-schema-spec.js').LocalRef} LocalRef */ + +/** @typedef {import('./helpers/json-pointer-spec.js').JsonPointer} JsonPointer */ + /** @typedef {import('./helpers/json-schema-spec.js').Schema} Schema */ /** * @typedef {object} BundleResult * @property {Schema} schema - * @property {string[]} embedded embedded schema files - * @property {Array<[string, string]>} external external refs + * @property {string[]} external external refs + * @property {Array<[string, LocalRef]>} embedded embedded schema files * @property {Array<[string, string]>} rewired rewired refs */ @@ -97,7 +103,7 @@ export const bundle = async (entryFile, includeFiles, targetFile) => { /** * Keys are source schema ref with absolute fs path. * Values are target schema local ref. - * @type {Map} + * @type {Map} */ const embeddedMap = new Map(); /** @@ -144,9 +150,9 @@ export const bundle = async (entryFile, includeFiles, targetFile) => { } rewiredRS = { path: null, - frag: (refFile === entryFile && !frag?.startsWith(FRAG_DEFS_PREFIX) + frag: (refFile === entryFile && !frag?.startsWith(JP_DEFS_PREFIX) ? ROOT_POINTER - : `${FRAG_DEFS_PREFIX}${escapeJsonPointer(moduleName)}` + : `${JP_DEFS_PREFIX}${escapeJsonPointer(moduleName)}` ) + (frag ?? '') }; } else { @@ -184,7 +190,10 @@ export const bundle = async (entryFile, includeFiles, targetFile) => { [DEFS_KEYWORD]: schemaDefsOrig, [NOT_KEYWORD]: {[COMMENT_KEYWORD]: HOLLOW_COMMENT} }; - embeddedMap.set(`${entryFile}${REF_FRAG_SEP}${JP_PREFIX}${DEFS_KEYWORD}`, `${REF_FRAG_SEP}${FRAG_DEFS_PREFIX}${escapeJsonPointer(entryModuleName)}`); + embeddedMap.set(refJoin({ + path: entryFile, + frag: JP_DEFS + }), `${JP_DEFS_PREFIX}${escapeJsonPointer(entryModuleName)}`); } else { delete schema[DEFS_KEYWORD][entryModuleName]; } @@ -198,7 +207,7 @@ export const bundle = async (entryFile, includeFiles, targetFile) => { delete includeSchema[COMMENT_KEYWORD]; rewireRefs(includeSchema, includeFile); schema[DEFS_KEYWORD][moduleName] = includeSchema; - embeddedMap.set(includeFile, `${REF_FRAG_SEP}${FRAG_DEFS_PREFIX}${escapeJsonPointer(moduleName)}`); + embeddedMap.set(includeFile, `${JP_DEFS_PREFIX}${escapeJsonPointer(moduleName)}`); } return { @@ -207,12 +216,16 @@ export const bundle = async (entryFile, includeFiles, targetFile) => { embedded: Array.from(embeddedMap, ([f, d]) => { const {path, frag} = refSplit(f); - return [refJoin({path: posixPath(relative(entryDir, path)), frag}), d]; + return [ + refJoin({path: posixPath(relative(entryDir, path)), frag}), + makeLocalRef(d)]; }), rewired: Array.from(rewiredMap, ([f, t]) => { const {path, frag} = refSplit(f); - return [refJoin({path: posixPath(relative(entryDir, path)), frag}), t]; + return [ + refJoin({path: posixPath(relative(entryDir, path)), frag}), + t]; } ) }; diff --git a/tools/src/main/js/bundler/src/helpers/json-schema-spec.js b/tools/src/main/js/bundler/src/helpers/json-schema-spec.js index 2c389498b..fffcfac91 100644 --- a/tools/src/main/js/bundler/src/helpers/json-schema-spec.js +++ b/tools/src/main/js/bundler/src/helpers/json-schema-spec.js @@ -5,11 +5,13 @@ * Deprecated keywords are ignored. */ + /** * Json Schema * @typedef {Record} Schema */ + export const SCHEMA_KEYWORD = '$schema'; export const ID_KEYWORD = '$id'; @@ -88,7 +90,7 @@ export const subschemas = function* (s) { }; -export const REF_FRAG_SEP = '#'; +const REF_FRAG_SEP = '#'; /** * @typedef {object} RefSplit @@ -139,8 +141,26 @@ const URI_OR_ABS_WIN_RE = Object.freeze(/^[a-z][a-z0-9+.-]*:/i); * @returns {boolean} */ export const refIsAbsolute = (r) => { - if (r.startsWith(REF_FRAG_SEP)) return false; + if (refIsLocal(r)) return false; return r.startsWith('/') // Unix-like absolute path || r.startsWith('\\') // UNC-like absolute path || URI_OR_ABS_WIN_RE.test(r); }; + +/** + * Local reference: a JSON Pointer prefixed with {@link REF_FRAG_SEP}. + * @typedef {`${typeof REF_FRAG_SEP}${string}`} LocalRef + */ + +/** + * Whether a ref is local. + * @param {string} r ref + * @return {r is LocalRef} + */ +export const refIsLocal = (r) => r.startsWith(REF_FRAG_SEP) + +/** + * @param {string} s + * @return {LocalRef} + */ +export const makeLocalRef = (s) => `${REF_FRAG_SEP}${s}`; diff --git a/tools/src/main/js/bundler/src/sanity.js b/tools/src/main/js/bundler/src/sanity.js index 82bb745d4..f8912c967 100644 --- a/tools/src/main/js/bundler/src/sanity.js +++ b/tools/src/main/js/bundler/src/sanity.js @@ -1,5 +1,12 @@ import {objectGetOwnPath, PATH_NOT_FOUND} from "./helpers/common.js"; -import {isSchemaObject, REF_FRAG_SEP, REF_KEYWORDS, subschemas} from './helpers/json-schema-spec.js'; +import { + isSchemaObject, + makeLocalRef, + REF_KEYWORDS, + refIsLocal, + refSplit, + subschemas +} from './helpers/json-schema-spec.js'; import {isJsonPointer, jsonPointer2stack} from './helpers/json-pointer-spec.js' /** @typedef {import('./helpers/json-schema-spec.js').Schema} Schema */ @@ -45,8 +52,8 @@ const validateRefs = (schema) => { for (const k of REF_KEYWORDS) { const v = schema[k]; if (typeof v !== 'string') continue; - if (v.startsWith(REF_FRAG_SEP)) { - localRefs.add(v.slice(REF_FRAG_SEP.length)); + if (refIsLocal(v)) { + localRefs.add(refSplit(v).frag); } else { warnings.add(`Unchecked external ref: ${v}`); } @@ -60,16 +67,16 @@ const validateRefs = (schema) => { if (isJsonPointer(ref)) { const rTarget = objectGetOwnPath(schema, jsonPointer2stack(ref)); if (rTarget === PATH_NOT_FOUND) { - errors.add(`Missing local ref: ${REF_FRAG_SEP}${ref}`); + errors.add(`Missing local ref target: ${makeLocalRef(ref)}`); } if (isSchemaObject(rTarget)) { gatherRefs(rTarget); } else if (typeof rTarget !== 'boolean') { // ref must resolve to a schema or boolean; - throw new RangeError(`Illegal local ref target: ${REF_FRAG_SEP}${ref}`); + throw new RangeError(`Illegal local ref target: ${makeLocalRef(ref)}`); } } else { - warnings.add(`Unchecked local ref: ${REF_FRAG_SEP}${ref}`); + warnings.add(`Unchecked local ref: ${makeLocalRef(ref)}`); } } From 1a6c391994affabd2159ed22f244ec74d623ed32 Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Fri, 23 Oct 2026 17:31:00 +0200 Subject: [PATCH 3/6] chore(bundler): fix Signed-off-by: Jan Kowalleck --- tools/src/main/js/bundler/src/sanity.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/src/main/js/bundler/src/sanity.js b/tools/src/main/js/bundler/src/sanity.js index f8912c967..343511a44 100644 --- a/tools/src/main/js/bundler/src/sanity.js +++ b/tools/src/main/js/bundler/src/sanity.js @@ -72,8 +72,8 @@ const validateRefs = (schema) => { if (isSchemaObject(rTarget)) { gatherRefs(rTarget); } else if (typeof rTarget !== 'boolean') { - // ref must resolve to a schema or boolean; - throw new RangeError(`Illegal local ref target: ${makeLocalRef(ref)}`); + // ref must resolve to a schema or literal boolean + errors.add(`Illegal (non-schema) local ref target: ${makeLocalRef(ref)}`); } } else { warnings.add(`Unchecked local ref: ${makeLocalRef(ref)}`); From 33c3eb71853a736bf0f5a5cd3573a94ee37e3b59 Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Fri, 23 Oct 2026 17:35:37 +0200 Subject: [PATCH 4/6] tidy Signed-off-by: Jan Kowalleck --- tools/src/main/js/bundler/src/helpers/json-schema-spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/src/main/js/bundler/src/helpers/json-schema-spec.js b/tools/src/main/js/bundler/src/helpers/json-schema-spec.js index fffcfac91..514607c8e 100644 --- a/tools/src/main/js/bundler/src/helpers/json-schema-spec.js +++ b/tools/src/main/js/bundler/src/helpers/json-schema-spec.js @@ -157,7 +157,7 @@ export const refIsAbsolute = (r) => { * @param {string} r ref * @return {r is LocalRef} */ -export const refIsLocal = (r) => r.startsWith(REF_FRAG_SEP) +export const refIsLocal = (r) => r.startsWith(REF_FRAG_SEP); /** * @param {string} s From 209b368a414318e51391cc15fa169cd69376c204 Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Fri, 23 Oct 2026 17:37:41 +0200 Subject: [PATCH 5/6] tidy Signed-off-by: Jan Kowalleck --- .../bundler/src/helpers/json-schema-spec.js | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/tools/src/main/js/bundler/src/helpers/json-schema-spec.js b/tools/src/main/js/bundler/src/helpers/json-schema-spec.js index 514607c8e..6481fce24 100644 --- a/tools/src/main/js/bundler/src/helpers/json-schema-spec.js +++ b/tools/src/main/js/bundler/src/helpers/json-schema-spec.js @@ -90,7 +90,7 @@ export const subschemas = function* (s) { }; -const REF_FRAG_SEP = '#'; +const FRAG_SEP = '#'; /** * @typedef {object} RefSplit @@ -107,9 +107,9 @@ const REF_FRAG_SEP = '#'; * @returns {RefSplit} */ export const refSplit = (r) => { - const split = r.split(REF_FRAG_SEP); + const split = r.split(FRAG_SEP); if (split.length > 2) { - throw new RangeError(`Unexpected amount of "${REF_FRAG_SEP}" in ${r}`); + throw new RangeError(`Unexpected amount of "${FRAG_SEP}" in ${r}`); } return { path: split[0] || null, @@ -124,10 +124,10 @@ export const refSplit = (r) => { * @returns {string} */ export const refJoin = ({path, frag}) => { - if (path && frag) return `${path}${REF_FRAG_SEP}${frag}`; + if (path && frag) return `${path}${FRAG_SEP}${frag}`; if (path) return path; - if (frag) return `${REF_FRAG_SEP}${frag}`; - return REF_FRAG_SEP; // the entire schema + if (frag) return `${FRAG_SEP}${frag}`; + return FRAG_SEP; // the entire schema }; /** @@ -148,8 +148,8 @@ export const refIsAbsolute = (r) => { }; /** - * Local reference: a JSON Pointer prefixed with {@link REF_FRAG_SEP}. - * @typedef {`${typeof REF_FRAG_SEP}${string}`} LocalRef + * Local reference: a JSON Pointer prefixed with {@link FRAG_SEP}. + * @typedef {`${typeof FRAG_SEP}${string}`} LocalRef */ /** @@ -157,10 +157,10 @@ export const refIsAbsolute = (r) => { * @param {string} r ref * @return {r is LocalRef} */ -export const refIsLocal = (r) => r.startsWith(REF_FRAG_SEP); +export const refIsLocal = (r) => r.startsWith(FRAG_SEP); /** * @param {string} s * @return {LocalRef} */ -export const makeLocalRef = (s) => `${REF_FRAG_SEP}${s}`; +export const makeLocalRef = (s) => `${FRAG_SEP}${s}`; From fa5a856640cd8495ff7a23ae4917a82987234363 Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Fri, 23 Oct 2026 17:41:28 +0200 Subject: [PATCH 6/6] tidy Signed-off-by: Jan Kowalleck --- tools/src/main/js/bundler/src/sanity.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/src/main/js/bundler/src/sanity.js b/tools/src/main/js/bundler/src/sanity.js index 343511a44..127542e82 100644 --- a/tools/src/main/js/bundler/src/sanity.js +++ b/tools/src/main/js/bundler/src/sanity.js @@ -7,7 +7,7 @@ import { refSplit, subschemas } from './helpers/json-schema-spec.js'; -import {isJsonPointer, jsonPointer2stack} from './helpers/json-pointer-spec.js' +import {isJsonPointer, jsonPointer2stack, ROOT_POINTER} from './helpers/json-pointer-spec.js' /** @typedef {import('./helpers/json-schema-spec.js').Schema} Schema */ @@ -53,7 +53,7 @@ const validateRefs = (schema) => { const v = schema[k]; if (typeof v !== 'string') continue; if (refIsLocal(v)) { - localRefs.add(refSplit(v).frag); + localRefs.add(refSplit(v).frag ?? ROOT_POINTER); } else { warnings.add(`Unchecked external ref: ${v}`); }