From e3db123afd0b155e26a5f152720525c913dd3121 Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Mon, 24 Aug 2026 18:44:39 +0200 Subject: [PATCH 1/2] [2.0] linter: removed functionality that is already done by other tests additionalProperties is taken care of by `tools/src/test/js/schema-v2/json-schema-semantic-tests.js` Signed-off-by: Jan Kowalleck --- .../additional-properties-false.check.js | 123 ------------------ tools/src/main/js/linter/checks/index.js | 9 +- 2 files changed, 4 insertions(+), 128 deletions(-) delete mode 100644 tools/src/main/js/linter/checks/additional-properties-false.check.js diff --git a/tools/src/main/js/linter/checks/additional-properties-false.check.js b/tools/src/main/js/linter/checks/additional-properties-false.check.js deleted file mode 100644 index a7bf29249..000000000 --- a/tools/src/main/js/linter/checks/additional-properties-false.check.js +++ /dev/null @@ -1,123 +0,0 @@ -/** - * CycloneDX Schema Linter - Additional Properties False Check - * - * Validates that all object definitions have `additionalProperties: false`. - * This ensures strict schema validation and prevents unexpected properties. - * - * @license Apache-2.0 - */ - -import { LintCheck, registerCheck, Severity, traverseSchema } from '../index.js'; - -/** - * Check that validates additionalProperties is set to false on objects - */ -class AdditionalPropertiesFalseCheck extends LintCheck { - constructor() { - super( - 'additional-properties-false', - 'Additional Properties False', - 'Validates that object definitions have additionalProperties set to false.', - Severity.ERROR - ); - } - - async run(schema, rawContent, config = {}) { - const issues = []; - - // Paths to exclude from checking (e.g., extension points) - const excludePaths = config.excludePaths || []; - - // Allow additionalProperties to be a schema (for pattern-based validation) - const allowSchema = config.allowSchema ?? false; - - traverseSchema(schema, (node, path, key, parent) => { - // Only check objects with 'properties' defined - if (typeof node !== 'object' || node === null || Array.isArray(node)) { - return; - } - - // Must have type: "object" or properties defined to be considered an object schema - const isObjectSchema = node.type === 'object' || - node.properties !== undefined || - node.patternProperties !== undefined; - - if (!isObjectSchema) return; - - // Skip excluded paths - if (excludePaths.some(excluded => path.includes(excluded))) return; - - // Skip if this is a $ref (references are validated separately) - if (node.$ref) return; - - // Skip certain schema composition keywords that don't need additionalProperties - if (key === 'if' || key === 'then' || key === 'else') return; - if (key === 'not') return; - - // Check additionalProperties - if (!('additionalProperties' in node)) { - // additionalProperties is not defined - if (node.properties || node.patternProperties) { - issues.push(this.createIssue( - 'Object schema is missing "additionalProperties: false".', - path, - { - hasProperties: !!node.properties, - hasPatternProperties: !!node.patternProperties, - suggestion: 'Add "additionalProperties": false to prevent unexpected properties.' - } - )); - } - } else if (node.additionalProperties === true) { - // Explicitly set to true - issues.push(this.createIssue( - 'Object schema has "additionalProperties: true". Set to false for strict validation.', - path, - { - current: true, - suggestion: 'Change "additionalProperties" to false.' - } - )); - } else if (node.additionalProperties !== false) { - // Set to a schema object - if (!allowSchema) { - // Check if it's an empty object (equivalent to true) - if (typeof node.additionalProperties === 'object' && - Object.keys(node.additionalProperties).length === 0) { - issues.push(this.createIssue( - 'Object schema has "additionalProperties: {}" which is equivalent to true. Set to false.', - path, - { - current: '{}', - suggestion: 'Change "additionalProperties" to false.' - }, - Severity.WARNING - )); - } else { - // It's a schema - this might be intentional - issues.push(this.createIssue( - 'Object schema has "additionalProperties" set to a schema. ' + - 'Consider using false unless additional properties are intentionally allowed.', - path, - { - current: typeof node.additionalProperties, - suggestion: 'Review whether additional properties should be allowed.' - }, - Severity.INFO - )); - } - } - } - // else: additionalProperties === false, which is correct - }); - - return issues; - } -} - -// Create and register the check -const check = new AdditionalPropertiesFalseCheck(); -registerCheck(check); - -export { AdditionalPropertiesFalseCheck }; -export default check; diff --git a/tools/src/main/js/linter/checks/index.js b/tools/src/main/js/linter/checks/index.js index 6483367da..f2fe5f69c 100644 --- a/tools/src/main/js/linter/checks/index.js +++ b/tools/src/main/js/linter/checks/index.js @@ -1,9 +1,9 @@ /** * CycloneDX Schema Linter - Check Module Loader - * + * * This module loads all check modules from the checks directory. * Each check is automatically registered with the linter. - * + * * @license Apache-2.0 */ @@ -21,7 +21,7 @@ export async function loadAllChecks() { const checkFiles = readdirSync(__dirname).filter( file => file.endsWith('.check.js') ); - + const loadPromises = checkFiles.map(async file => { const modulePath = join(__dirname, file); try { @@ -30,7 +30,7 @@ export async function loadAllChecks() { console.error(`Failed to load check module ${file}: ${err.message}`); } }); - + await Promise.all(loadPromises); } @@ -47,7 +47,6 @@ export * from './property-name-american-english.check.js'; export * from './description-oxford-english.check.js'; export * from './no-uppercase-rfc.check.js'; export * from './no-must-word.check.js'; -export * from './additional-properties-false.check.js'; export * from './title-formatting.check.js'; export * from './enum-value-formatting.check.js'; export * from './ref-usage.check.js'; From 4a194f39d6090f8e0dfb731bacb5b954ca902b4a Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Mon, 24 Aug 2026 20:30:09 +0200 Subject: [PATCH 2/2] remove $ref best practies - they were superseded by the existing tests Signed-off-by: Jan Kowalleck --- tools/src/main/js/linter/checks/index.js | 1 - .../main/js/linter/checks/ref-usage.check.js | 142 ------------------ 2 files changed, 143 deletions(-) delete mode 100644 tools/src/main/js/linter/checks/ref-usage.check.js diff --git a/tools/src/main/js/linter/checks/index.js b/tools/src/main/js/linter/checks/index.js index f2fe5f69c..e56dc0c4e 100644 --- a/tools/src/main/js/linter/checks/index.js +++ b/tools/src/main/js/linter/checks/index.js @@ -49,6 +49,5 @@ export * from './no-uppercase-rfc.check.js'; export * from './no-must-word.check.js'; export * from './title-formatting.check.js'; export * from './enum-value-formatting.check.js'; -export * from './ref-usage.check.js'; export * from './duplicate-content.check.js'; export * from './duplicate-definitions.check.js'; diff --git a/tools/src/main/js/linter/checks/ref-usage.check.js b/tools/src/main/js/linter/checks/ref-usage.check.js deleted file mode 100644 index 50c56fd76..000000000 --- a/tools/src/main/js/linter/checks/ref-usage.check.js +++ /dev/null @@ -1,142 +0,0 @@ -/** - * CycloneDX Schema Linter - Ref Usage Check - * - * Validates that $ref usage follows best practices: - * - $ref should not be combined with other keywords (draft-07 limitation) - * - Referenced definitions should exist - * - No circular references (configurable) - * - * @license Apache-2.0 - */ - -import { LintCheck, registerCheck, Severity, traverseSchema } from '../index.js'; - -/** - * Keywords that should not appear alongside $ref in draft-07 - */ -const CONFLICTING_KEYWORDS = [ - 'type', 'properties', 'additionalProperties', 'required', - 'items', 'additionalItems', 'contains', - 'minimum', 'maximum', 'exclusiveMinimum', 'exclusiveMaximum', - 'minLength', 'maxLength', 'pattern', 'format', - 'minItems', 'maxItems', 'uniqueItems', - 'minProperties', 'maxProperties', - 'enum', 'const', 'default', - 'allOf', 'anyOf', 'oneOf', 'not', 'if', 'then', 'else' -]; - -/** - * Keywords allowed alongside $ref - */ -const ALLOWED_KEYWORDS = [ - '$ref', 'title', 'description', '$comment', 'examples' -]; - -/** - * Check that validates $ref usage - */ -class RefUsageCheck extends LintCheck { - constructor() { - super( - 'ref-usage', - 'Ref Usage', - 'Validates that $ref usage follows JSON Schema best practices.', - Severity.WARNING - ); - } - - async run(schema, rawContent, config = {}) { - const issues = []; - - const checkConflictingKeywords = config.checkConflictingKeywords ?? true; - const checkDefinitionsExist = config.checkDefinitionsExist ?? true; - const allowedConflicting = new Set(config.allowedConflicting || ['title', 'description']); - - // Collect all definitions - const definitions = new Set(); - const definitionPaths = ['definitions', '$defs']; - - for (const defPath of definitionPaths) { - if (schema[defPath]) { - for (const defName of Object.keys(schema[defPath])) { - definitions.add(`#/${defPath}/${defName}`); - } - } - } - - // Track all $ref usages - const refUsages = []; - - traverseSchema(schema, (node, path, key, parent) => { - // Check $ref nodes - if (key === '$ref' && typeof node === 'string') { - refUsages.push({ ref: node, path, parent }); - } - - // Check for $ref combined with other keywords - if (typeof node === 'object' && node !== null && !Array.isArray(node) && node.$ref) { - if (checkConflictingKeywords) { - const keys = Object.keys(node); - const conflicting = keys.filter(k => - !ALLOWED_KEYWORDS.includes(k) && - !allowedConflicting.has(k) - ); - - if (conflicting.length > 0) { - issues.push(this.createIssue( - `$ref is combined with other keywords: ${conflicting.join(', ')}. ` + - `In JSON Schema draft-07, $ref causes other keywords to be ignored.`, - path, - { - ref: node.$ref, - conflictingKeywords: conflicting - } - )); - } - } - } - }); - - // Check that referenced definitions exist - if (checkDefinitionsExist) { - for (const { ref, path } of refUsages) { - // Only check local references - if (ref.startsWith('#/')) { - if (!definitions.has(ref)) { - // Check if it's a valid path in the schema - const refPath = ref.substring(2).split('/'); - let target = schema; - let valid = true; - - for (const segment of refPath) { - if (target && typeof target === 'object' && segment in target) { - target = target[segment]; - } else { - valid = false; - break; - } - } - - if (!valid) { - issues.push(this.createIssue( - `$ref "${ref}" references a non-existent definition.`, - path, - { ref }, - Severity.ERROR - )); - } - } - } - } - } - - return issues; - } -} - -// Create and register the check -const check = new RefUsageCheck(); -registerCheck(check); - -export { RefUsageCheck }; -export default check;