diff --git a/packages/core/src/ruleset/__tests__/ruleset.test.ts b/packages/core/src/ruleset/__tests__/ruleset.test.ts index 7bfdb8130..7068adb21 100644 --- a/packages/core/src/ruleset/__tests__/ruleset.test.ts +++ b/packages/core/src/ruleset/__tests__/ruleset.test.ts @@ -262,7 +262,11 @@ describe('Ruleset', () => { describe('error handling', () => { it('given empty ruleset, should throw a user friendly error', () => { expect(() => new Ruleset({})).toThrowError( - new RulesetValidationError('Ruleset must have rules or extends or overrides defined', []), + new RulesetValidationError( + 'invalid-ruleset-definition', + 'Ruleset must have rules or extends or overrides defined', + [], + ), ); }); }); @@ -1271,7 +1275,11 @@ describe('Ruleset', () => { }), ).toThrowAggregateError( new AggregateError([ - new RulesetValidationError('Alias "PathItem-" does not exist', ['rules', 'valid-path', 'given']), + new RulesetValidationError('undefined-alias', 'Alias "PathItem-" does not exist', [ + 'rules', + 'valid-path', + 'given', + ]), ]), ); }); @@ -1298,6 +1306,7 @@ describe('Ruleset', () => { ).toThrowAggregateError( new AggregateError([ new RulesetValidationError( + 'generic-validation-error', 'Alias "Test" is circular. Resolution stack: Test -> Contact -> Info -> Root -> Info', ['rules', 'valid-path', 'given'], ), @@ -1335,9 +1344,17 @@ describe('Ruleset', () => { }), ).toThrowAggregateError( new AggregateError([ - new RulesetValidationError('Alias "PathItem" does not exist', ['rules', 'valid-path', 'given']), - new RulesetValidationError('Alias "Name" does not exist', ['rules', 'valid-name-and-description', 'given']), - new RulesetValidationError(`Alias "Description" does not exist`, [ + new RulesetValidationError('undefined-alias', 'Alias "PathItem" does not exist', [ + 'rules', + 'valid-path', + 'given', + ]), + new RulesetValidationError('undefined-alias', 'Alias "Name" does not exist', [ + 'rules', + 'valid-name-and-description', + 'given', + ]), + new RulesetValidationError('undefined-alias', `Alias "Description" does not exist`, [ 'rules', 'valid-name-and-description', 'given', diff --git a/packages/core/src/ruleset/alias.ts b/packages/core/src/ruleset/alias.ts index b70de064a..fa806961b 100644 --- a/packages/core/src/ruleset/alias.ts +++ b/packages/core/src/ruleset/alias.ts @@ -44,12 +44,12 @@ function _resolveAlias( const alias = ALIAS.exec(expression)?.[1]; if (alias === void 0 || alias === null) { - throw new ReferenceError(`Alias must match /^#([A-Za-z0-9_-]+)/`); + throw new TypeError(`Alias must match /^#([A-Za-z0-9_-]+)/`); } if (stack.has(alias)) { const _stack = [...stack, alias]; - throw new ReferenceError(`Alias "${_stack[0]}" is circular. Resolution stack: ${_stack.join(' -> ')}`); + throw new Error(`Alias "${_stack[0]}" is circular. Resolution stack: ${_stack.join(' -> ')}`); } stack.add(alias); diff --git a/packages/core/src/ruleset/function.ts b/packages/core/src/ruleset/function.ts index 14a91de5a..b187fc13b 100644 --- a/packages/core/src/ruleset/function.ts +++ b/packages/core/src/ruleset/function.ts @@ -19,7 +19,11 @@ addFormats(ajv); export class RulesetFunctionValidationError extends RulesetValidationError { constructor(fn: string, error: ErrorObject) { - super(RulesetFunctionValidationError.printMessage(fn, error), error.instancePath.slice(1).split('/')); + super( + 'invalid-function-options', + RulesetFunctionValidationError.printMessage(fn, error), + error.instancePath.slice(1).split('/'), + ); } private static printMessage(fn: string, error: ErrorObject): string { @@ -150,7 +154,11 @@ export function createRulesetFunction( } if (options === null) { - throw new RulesetValidationError(`"${fn.name || ''}" function does not accept any options`, []); + throw new RulesetValidationError( + 'invalid-function-options', + `"${fn.name || ''}" function does not accept any options`, + [], + ); } else if ( 'errors' in validateOptions && Array.isArray(validateOptions.errors) && @@ -160,7 +168,11 @@ export function createRulesetFunction( validateOptions.errors.map(error => new RulesetFunctionValidationError(fn.name || '', error)), ); } else { - throw new RulesetValidationError(`"functionOptions" of "${fn.name || ''}" function must be valid`, []); + throw new RulesetValidationError( + 'invalid-function-options', + `"functionOptions" of "${fn.name || ''}" function must be valid`, + [], + ); } }; diff --git a/packages/core/src/ruleset/meta/ruleset.schema.json b/packages/core/src/ruleset/meta/ruleset.schema.json index 553aa01fc..c59654879 100644 --- a/packages/core/src/ruleset/meta/ruleset.schema.json +++ b/packages/core/src/ruleset/meta/ruleset.schema.json @@ -101,7 +101,7 @@ }, "required": ["files"], "errorMessage": { - "type": "must be a override, i.e. { \"files\": [\"v2/**/*.json\"], \"rules\": {} }" + "type": "must be an override, i.e. { \"files\": [\"v2/**/*.json\"], \"rules\": {} }" } }, { diff --git a/packages/core/src/ruleset/validation/__tests__/validation.test.ts b/packages/core/src/ruleset/validation/__tests__/validation.test.ts index a729f2f68..a78f8eb60 100644 --- a/packages/core/src/ruleset/validation/__tests__/validation.test.ts +++ b/packages/core/src/ruleset/validation/__tests__/validation.test.ts @@ -40,20 +40,21 @@ describe('JS Ruleset Validation', () => { it('given invalid ruleset, throws', () => { expect(assertValidRuleset.bind(null, invalidRuleset)).toThrowAggregateError( new AggregateError([ - new RulesetValidationError('the rule must have at least "given" and "then" properties', [ - 'rules', - 'no-given-no-then', - ]), - new RulesetValidationError('allowed types are "style" and "validation"', [ + new RulesetValidationError( + 'invalid-rule-definition', + 'the rule must have at least "given" and "then" properties', + ['rules', 'no-given-no-then'], + ), + new RulesetValidationError('invalid-rule-definition', 'allowed types are "style" and "validation"', [ 'rules', 'rule-with-invalid-enum', 'type', ]), - new RulesetValidationError('the value has to be one of: 0, 1, 2, 3 or "error", "warn", "info", "hint", "off"', [ - 'rules', - 'rule-with-invalid-enum', - 'severity', - ]), + new RulesetValidationError( + 'invalid-severity', + 'the value has to be one of: 0, 1, 2, 3 or "error", "warn", "info", "hint", "off"', + ['rules', 'rule-with-invalid-enum', 'severity'], + ), ]), ); }); @@ -66,7 +67,9 @@ describe('JS Ruleset Validation', () => { 'given invalid %s documentationUrl in a rule, throws', documentationUrl => { expect(assertValidRuleset.bind(null, { documentationUrl, rules: {} })).toThrowAggregateError( - new AggregateError([new RulesetValidationError('must be a valid URL', ['documentationUrl'])]), + new AggregateError([ + new RulesetValidationError('generic-validation-error', 'must be a valid URL', ['documentationUrl']), + ]), ); expect( @@ -82,7 +85,13 @@ describe('JS Ruleset Validation', () => { }, }), ).toThrowAggregateError( - new AggregateError([new RulesetValidationError('must be a valid URL', ['rules', 'rule', 'documentationUrl'])]), + new AggregateError([ + new RulesetValidationError('invalid-rule-definition', 'must be a valid URL', [ + 'rules', + 'rule', + 'documentationUrl', + ]), + ]), ); }, ); @@ -154,13 +163,23 @@ describe('JS Ruleset Validation', () => { it.each<[unknown, RulesetValidationError[]]>([ [ [[{ rules: {} }, 'test']], - [new RulesetValidationError('allowed types are "off", "recommended" and "all"', ['extends', '0', '1'])], + [ + new RulesetValidationError('invalid-extend-definition', 'allowed types are "off", "recommended" and "all"', [ + 'extends', + '0', + '1', + ]), + ], ], [ [[{ rules: {} }, 'test'], 'foo'], [ - new RulesetValidationError('must be a valid ruleset', ['extends', '1']), - new RulesetValidationError('allowed types are "off", "recommended" and "all"', ['extends', '0', '1']), + new RulesetValidationError('invalid-extend-definition', 'must be a valid ruleset', ['extends', '1']), + new RulesetValidationError('invalid-extend-definition', 'allowed types are "off", "recommended" and "all"', [ + 'extends', + '0', + '1', + ]), ], ], ])('recognizes invalid array-ish extends syntax %p', (_extends, errors) => { @@ -184,12 +203,20 @@ describe('JS Ruleset Validation', () => { [ [2, 'a'], new AggregateError([ - new RulesetValidationError('must be a valid format', ['formats', '0']), - new RulesetValidationError('must be a valid format', ['formats', '1']), + new RulesetValidationError('invalid-format', 'must be a valid format', ['formats', '0']), + new RulesetValidationError('invalid-format', 'must be a valid format', ['formats', '1']), + ]), + ], + [ + 2, + new AggregateError([ + new RulesetValidationError('invalid-ruleset-definition', 'must be an array of formats', ['formats']), ]), ], - [2, new AggregateError([new RulesetValidationError('must be an array of formats', ['formats'])])], - [[''], new AggregateError([new RulesetValidationError('must be a valid format', ['formats', '0'])])], + [ + [''], + new AggregateError([new RulesetValidationError('invalid-format', 'must be a valid format', ['formats', '0'])]), + ], ])('recognizes invalid ruleset %p formats syntax', (formats, error) => { expect( assertValidRuleset.bind(null, { @@ -220,11 +247,20 @@ describe('JS Ruleset Validation', () => { [ [2, 'a'], new AggregateError([ - new RulesetValidationError('must be a valid format', ['rules', 'rule', 'formats', '0']), - new RulesetValidationError('must be a valid format', ['rules', 'rule', 'formats', '1']), + new RulesetValidationError('invalid-format', 'must be a valid format', ['rules', 'rule', 'formats', '0']), + new RulesetValidationError('invalid-format', 'must be a valid format', ['rules', 'rule', 'formats', '1']), + ]), + ], + [ + 2, + new AggregateError([ + new RulesetValidationError('invalid-rule-definition', 'must be an array of formats', [ + 'rules', + 'rule', + 'formats', + ]), ]), ], - [2, new AggregateError([new RulesetValidationError('must be an array of formats', ['rules', 'rule', 'formats'])])], ])('recognizes invalid rule %p formats syntax', (formats, error) => { expect( assertValidRuleset.bind(null, { @@ -247,7 +283,9 @@ describe('JS Ruleset Validation', () => { assertValidRuleset.bind(null, { overrides: null, }), - ).toThrowAggregateError(new AggregateError([new RulesetValidationError('must be array', ['overrides'])])); + ).toThrowAggregateError( + new AggregateError([new RulesetValidationError('invalid-ruleset-definition', 'must be array', ['overrides'])]), + ); }); it('given an empty overrides, throws', () => { @@ -255,7 +293,11 @@ describe('JS Ruleset Validation', () => { assertValidRuleset.bind(null, { overrides: [], }), - ).toThrowAggregateError(new AggregateError([new RulesetValidationError('must not be empty', ['overrides'])])); + ).toThrowAggregateError( + new AggregateError([ + new RulesetValidationError('invalid-override-definition', 'must not be empty', ['overrides']), + ]), + ); }); it('given an invalid pattern, throws', () => { @@ -265,10 +307,11 @@ describe('JS Ruleset Validation', () => { }), ).toThrowAggregateError( new AggregateError([ - new RulesetValidationError('must be a override, i.e. { "files": ["v2/**/*.json"], "rules": {} }', [ - 'overrides', - '0', - ]), + new RulesetValidationError( + 'invalid-override-definition', + 'must be an override, i.e. { "files": ["v2/**/*.json"], "rules": {} }', + ['overrides', '0'], + ), ]), ); }); @@ -281,25 +324,35 @@ describe('JS Ruleset Validation', () => { it.each<[Partial, RulesetValidationError]>([ [ { extends: [rulesetA] }, - new RulesetValidationError('must contain rules when JSON Pointers are defined', ['overrides', '0']), + new RulesetValidationError( + 'invalid-override-definition', + 'must contain rules when JSON Pointers are defined', + ['overrides', '0'], + ), ], [ { formats: [formatB] }, - new RulesetValidationError('must contain rules when JSON Pointers are defined', ['overrides', '0']), + new RulesetValidationError( + 'invalid-override-definition', + 'must contain rules when JSON Pointers are defined', + ['overrides', '0'], + ), ], [ { rules: {}, formats: [formatB] }, - new RulesetValidationError('must not override any other property than rules when JSON Pointers are defined', [ - 'overrides', - '0', - ]), + new RulesetValidationError( + 'invalid-override-definition', + 'must not override any other property than rules when JSON Pointers are defined', + ['overrides', '0'], + ), ], [ { rules: {}, extends: [rulesetA] }, - new RulesetValidationError('must not override any other property than rules when JSON Pointers are defined', [ - 'overrides', - '0', - ]), + new RulesetValidationError( + 'invalid-override-definition', + 'must not override any other property than rules when JSON Pointers are defined', + ['overrides', '0'], + ), ], [ { @@ -313,6 +366,7 @@ describe('JS Ruleset Validation', () => { }, }, new RulesetValidationError( + 'invalid-rule-definition', 'the value has to be one of: 0, 1, 2, 3 or "error", "warn", "info", "hint", "off"', ['overrides', '0', 'rules', 'definition'], ), @@ -388,7 +442,9 @@ describe('JS Ruleset Validation', () => { rules: {}, aliases: null, }), - ).toThrowAggregateError(new AggregateError([new RulesetValidationError('must be object', ['aliases'])])); + ).toThrowAggregateError( + new AggregateError([new RulesetValidationError('invalid-ruleset-definition', 'must be object', ['aliases'])]), + ); }); it.each([null, 5])('recognizes %p as an invalid type of aliases', value => { @@ -402,6 +458,7 @@ describe('JS Ruleset Validation', () => { ).toThrowAggregateError( new AggregateError([ new RulesetValidationError( + 'invalid-alias-definition', 'must be a valid JSON Path expression or a reference to the existing Alias optionally paired with a JSON Path expression subset', ['aliases', 'alias', '0'], ), @@ -420,6 +477,7 @@ describe('JS Ruleset Validation', () => { ).toThrowAggregateError( new AggregateError([ new RulesetValidationError( + 'invalid-alias-definition', 'to avoid confusion the name must match /^[A-Za-z][A-Za-z0-9_-]*$/ regular expression', ['aliases'], ), @@ -432,6 +490,7 @@ describe('JS Ruleset Validation', () => { [''], [ new RulesetValidationError( + 'invalid-alias-definition', 'must be a valid JSON Path expression or a reference to the existing Alias optionally paired with a JSON Path expression subset', ['aliases', 'PathItem', '0'], ), @@ -441,16 +500,26 @@ describe('JS Ruleset Validation', () => { ['foo'], [ new RulesetValidationError( + 'invalid-alias-definition', 'must be a valid JSON Path expression or a reference to the existing Alias optionally paired with a JSON Path expression subset', ['aliases', 'PathItem', '0'], ), ], ], - [[], [new RulesetValidationError('must be a non-empty array of expressions', ['aliases', 'PathItem'])]], + [ + [], + [ + new RulesetValidationError('invalid-alias-definition', 'must be a non-empty array of expressions', [ + 'aliases', + 'PathItem', + ]), + ], + ], [ [0], [ new RulesetValidationError( + 'invalid-alias-definition', 'must be a valid JSON Path expression or a reference to the existing Alias optionally paired with a JSON Path expression subset', ['aliases', 'PathItem', '0'], ), @@ -478,10 +547,11 @@ describe('JS Ruleset Validation', () => { }), ).toThrowAggregateError( new AggregateError([ - new RulesetValidationError('targets must be present and have at least a single alias definition', [ - 'aliases', - 'alias', - ]), + new RulesetValidationError( + 'invalid-alias-definition', + 'targets must be present and have at least a single alias definition', + ['aliases', 'alias'], + ), ]), ); }); @@ -551,7 +621,13 @@ describe('JS Ruleset Validation', () => { }, }), ).toThrowAggregateError( - new AggregateError([new RulesetValidationError('must be array', ['aliases', 'SchemaObject', 'targets'])]), + new AggregateError([ + new RulesetValidationError('invalid-alias-definition', 'must be array', [ + 'aliases', + 'SchemaObject', + 'targets', + ]), + ]), ); }); @@ -567,11 +643,11 @@ describe('JS Ruleset Validation', () => { }), ).toThrowAggregateError( new AggregateError([ - new RulesetValidationError('targets must have at least a single alias definition', [ - 'aliases', - 'SchemaObject', - 'targets', - ]), + new RulesetValidationError( + 'invalid-alias-definition', + 'targets must have at least a single alias definition', + ['aliases', 'SchemaObject', 'targets'], + ), ]), ); }); @@ -588,12 +664,11 @@ describe('JS Ruleset Validation', () => { }), ).toThrowAggregateError( new AggregateError([ - new RulesetValidationError('a valid target must contain given and non-empty formats', [ - 'aliases', - 'SchemaObject', - 'targets', - '0', - ]), + new RulesetValidationError( + 'invalid-alias-definition', + 'a valid target must contain given and non-empty formats', + ['aliases', 'SchemaObject', 'targets', '0'], + ), ]), ); }); @@ -619,7 +694,7 @@ describe('JS Ruleset Validation', () => { }), ).toThrowAggregateError( new AggregateError([ - new RulesetValidationError('must be a valid format', [ + new RulesetValidationError('invalid-format', 'must be a valid format', [ 'aliases', 'SchemaObject', 'targets', @@ -627,7 +702,7 @@ describe('JS Ruleset Validation', () => { 'formats', '0', ]), - new RulesetValidationError('must be a valid format', [ + new RulesetValidationError('invalid-format', 'must be a valid format', [ 'aliases', 'SchemaObject', 'targets', @@ -661,6 +736,7 @@ describe('JS Ruleset Validation', () => { ).toThrowAggregateError( new AggregateError([ new RulesetValidationError( + 'invalid-given-definition', 'must be a valid JSON Path expression or a reference to the existing Alias optionally paired with a JSON Path expression subset', ['aliases', 'SchemaObject', 'targets', '1', 'given', '0'], ), @@ -671,35 +747,52 @@ describe('JS Ruleset Validation', () => { }); describe('then validation', () => { - describe('custom function', () => { - it('given valid then, does not complain', () => { - expect( - assertValidRuleset.bind(null, { - rules: { - rule: { - given: '$', - then: { - function: truthy, - }, + it('given undefined function, throws', () => { + expect( + assertValidRuleset.bind(null, { + rules: { + rule: { + given: '$', + then: { + function: void 0, }, }, - }), - ).not.toThrow(); + }, + }), + ).toThrowAggregateError( + new AggregateError([ + new RulesetValidationError('undefined-function', 'Function is not defined', ['rules', 'rule', 'then']), + ]), + ); + }); - expect( - assertValidRuleset.bind(null, { - rules: { - rule: { - given: '$', - then: { - field: 'test', - function: truthy, - }, + it('given valid then, does not complain', () => { + expect( + assertValidRuleset.bind(null, { + rules: { + rule: { + given: '$', + then: { + function: truthy, }, }, - }), - ).not.toThrow(); - }); + }, + }), + ).not.toThrow(); + + expect( + assertValidRuleset.bind(null, { + rules: { + rule: { + given: '$', + then: { + field: 'test', + function: truthy, + }, + }, + }, + }), + ).not.toThrow(); }); }); @@ -734,13 +827,15 @@ describe('JS Ruleset Validation', () => { duplicateKeys: 'foo', }, }), - ).toThrowAggregateError( + ).toThrow( new AggregateError([ new RulesetValidationError( + 'invalid-parser-options-definition', 'the value has to be one of: 0, 1, 2, 3 or "error", "warn", "info", "hint", "off"', ['parserOptions', 'duplicateKeys'], ), new RulesetValidationError( + 'invalid-parser-options-definition', 'the value has to be one of: 0, 1, 2, 3 or "error", "warn", "info", "hint", "off"', ['parserOptions', 'incompatibleValues'], ), @@ -768,13 +863,23 @@ describe('JSON Ruleset Validation', () => { it.each<[unknown, RulesetValidationError[]]>([ [ [['test', 'test']], - [new RulesetValidationError('allowed types are "off", "recommended" and "all"', ['extends', '0', '1'])], + [ + new RulesetValidationError('invalid-extend-definition', 'allowed types are "off", "recommended" and "all"', [ + 'extends', + '0', + '1', + ]), + ], ], [ [['bar', 'test'], {}], [ - new RulesetValidationError('must be string', ['extends', '1']), - new RulesetValidationError(`allowed types are "off", "recommended" and "all"`, ['extends', '0', '1']), + new RulesetValidationError('invalid-extend-definition', 'must be string', ['extends', '1']), + new RulesetValidationError('invalid-extend-definition', `allowed types are "off", "recommended" and "all"`, [ + 'extends', + '0', + '1', + ]), ], ], ])('recognizes invalid array-ish extends syntax %p', (_extends, errors) => { @@ -806,12 +911,12 @@ describe('JSON Ruleset Validation', () => { [ [2, 'a'], [ - new RulesetValidationError('must be a valid format', ['formats', '0']), - new RulesetValidationError('must be a valid format', ['formats', '1']), + new RulesetValidationError('invalid-format', 'must be a valid format', ['formats', '0']), + new RulesetValidationError('invalid-format', 'must be a valid format', ['formats', '1']), ], ], - [2, [new RulesetValidationError('must be an array of formats', ['formats'])]], - [[''], [new RulesetValidationError('must be a valid format', ['formats', '0'])]], + [2, [new RulesetValidationError('invalid-ruleset-definition', 'must be an array of formats', ['formats'])]], + [[''], [new RulesetValidationError('invalid-format', 'must be a valid format', ['formats', '0'])]], ])('recognizes invalid ruleset %p formats syntax', (formats, errors) => { expect( assertValidRuleset.bind( @@ -850,11 +955,20 @@ describe('JSON Ruleset Validation', () => { [ [2, 'a'], [ - new RulesetValidationError('must be a valid format', ['rules', 'rule', 'formats', '0']), - new RulesetValidationError('must be a valid format', ['rules', 'rule', 'formats', '1']), + new RulesetValidationError('invalid-format', 'must be a valid format', ['rules', 'rule', 'formats', '0']), + new RulesetValidationError('invalid-format', 'must be a valid format', ['rules', 'rule', 'formats', '1']), + ], + ], + [ + 2, + [ + new RulesetValidationError('invalid-rule-definition', 'must be an array of formats', [ + 'rules', + 'rule', + 'formats', + ]), ], ], - [2, [new RulesetValidationError('must be an array of formats', ['rules', 'rule', 'formats'])]], ])('recognizes invalid rule %p formats syntax', (formats, errors) => { expect( assertValidRuleset.bind( diff --git a/packages/core/src/ruleset/validation/assertions.ts b/packages/core/src/ruleset/validation/assertions.ts index 704c1f34b..812f3b09e 100644 --- a/packages/core/src/ruleset/validation/assertions.ts +++ b/packages/core/src/ruleset/validation/assertions.ts @@ -9,11 +9,15 @@ export function assertValidRuleset( format: 'js' | 'json' = 'js', ): asserts ruleset is RulesetDefinition { if (!isPlainObject(ruleset)) { - throw new RulesetValidationError('Provided ruleset is not an object', []); + throw new RulesetValidationError('invalid-ruleset-definition', 'Provided ruleset is not an object', []); } if (!('rules' in ruleset) && !('extends' in ruleset) && !('overrides' in ruleset)) { - throw new RulesetValidationError('Ruleset must have rules or extends or overrides defined', []); + throw new RulesetValidationError( + 'invalid-ruleset-definition', + 'Ruleset must have rules or extends or overrides defined', + [], + ); } const validate = createValidator(format); @@ -29,6 +33,6 @@ function isRuleDefinition(rule: FileRuleDefinition): rule is RuleDefinition { export function assertValidRule(rule: FileRuleDefinition, name: string): asserts rule is RuleDefinition { if (!isRuleDefinition(rule)) { - throw new RulesetValidationError('Rule definition expected', ['rules', name]); + throw new RulesetValidationError('invalid-rule-definition', 'Rule definition expected', ['rules', name]); } } diff --git a/packages/core/src/ruleset/validation/errors.ts b/packages/core/src/ruleset/validation/errors.ts index a80e9da0a..af8660783 100644 --- a/packages/core/src/ruleset/validation/errors.ts +++ b/packages/core/src/ruleset/validation/errors.ts @@ -2,10 +2,31 @@ import type { ErrorObject } from 'ajv'; import type { IDiagnostic, JsonPath } from '@stoplight/types'; import { isAggregateError } from '../../guards/isAggregateError'; -type RulesetValidationSingleError = Pick; +export type RulesetValidationErrorCode = + | 'generic-validation-error' + | 'invalid-ruleset-definition' + | 'invalid-parser-options-definition' + | 'invalid-alias-definition' + | 'invalid-extend-definition' + | 'invalid-rule-definition' + | 'invalid-override-definition' + | 'invalid-function-options' + | 'invalid-given-definition' + | 'invalid-severity' + | 'invalid-format' + | 'undefined-function' + | 'undefined-alias'; -export class RulesetValidationError extends Error implements RulesetValidationSingleError { - constructor(public readonly message: string, public readonly path: JsonPath) { +interface IRulesetValidationSingleError extends Pick { + code: RulesetValidationErrorCode; +} + +export class RulesetValidationError extends Error implements IRulesetValidationSingleError { + constructor( + public readonly code: RulesetValidationErrorCode, + public readonly message: string, + public readonly path: JsonPath, + ) { super(message); } } @@ -55,11 +76,14 @@ export function convertAjvErrors(errors: ErrorObject[]): RulesetValidationError[ filteredErrors.push(error); } - return filteredErrors.flatMap(error => - error.keyword === 'x-spectral-runtime' - ? flatErrors(error.params.errors) - : new RulesetValidationError(error.message ?? 'unknown error', error.instancePath.slice(1).split('/')), - ); + return filteredErrors.flatMap(error => { + if (error.keyword === 'x-spectral-runtime') { + return flatErrors(error.params.errors); + } + + const path = error.instancePath.slice(1).split('/'); + return new RulesetValidationError(inferErrorCode(path, error.keyword), error.message ?? 'unknown error', path); + }); } function flatErrors(error: RulesetValidationError | AggregateError): RulesetValidationError | RulesetValidationError[] { @@ -69,3 +93,72 @@ function flatErrors(error: RulesetValidationError | AggregateError): RulesetVali return error; } + +function inferErrorCode(path: string[], keyword: string): RulesetValidationErrorCode { + if (path.length === 0) { + return 'generic-validation-error'; + } + + if (path.length === 1 && keyword !== 'errorMessage') { + return 'invalid-ruleset-definition'; + } + + switch (path[0]) { + case 'rules': + return inferErrorCodeFromRulesError(path); + case 'parserOptions': + return 'invalid-parser-options-definition'; + case 'aliases': + return inferErrorCodeFromAliasesError(path); + case 'extends': + return 'invalid-extend-definition'; + case 'overrides': + return inferErrorCodeFromOverrideError(path, keyword); + case 'formats': + if (path.length === 1) { + return 'invalid-ruleset-definition'; + } + + return 'invalid-format'; + default: + return 'generic-validation-error'; + } +} + +function inferErrorCodeFromRulesError(path: string[]): RulesetValidationErrorCode { + if (path.length === 3 && path[2] === 'severity') { + return 'invalid-severity'; + } + + if (path.length === 4 && path[2] === 'formats') { + return 'invalid-format'; + } + + if (path.length === 4 && path[2] === 'given') { + return 'invalid-given-definition'; + } + + return 'invalid-rule-definition'; +} + +function inferErrorCodeFromOverrideError(path: string[], keyword: string): RulesetValidationErrorCode { + if (path.length >= 3) { + return inferErrorCode(path.slice(2), keyword); + } + + return 'invalid-override-definition'; +} + +function inferErrorCodeFromAliasesError(path: string[]): RulesetValidationErrorCode { + if (path.length === 6) { + if (path[4] === 'given') { + return 'invalid-given-definition'; + } + + if (path[4] === 'formats') { + return 'invalid-format'; + } + } + + return 'invalid-alias-definition'; +} diff --git a/packages/core/src/ruleset/validation/index.ts b/packages/core/src/ruleset/validation/index.ts index b7eb566c4..1b87362e9 100644 --- a/packages/core/src/ruleset/validation/index.ts +++ b/packages/core/src/ruleset/validation/index.ts @@ -1,2 +1,2 @@ -export { RulesetValidationError } from './errors'; +export { RulesetValidationError, RulesetValidationErrorCode } from './errors'; export { assertValidRuleset } from './assertions'; diff --git a/packages/core/src/ruleset/validation/validators/alias.ts b/packages/core/src/ruleset/validation/validators/alias.ts index 2d6f494e0..e7de053a1 100644 --- a/packages/core/src/ruleset/validation/validators/alias.ts +++ b/packages/core/src/ruleset/validation/validators/alias.ts @@ -2,7 +2,8 @@ import { isPlainObject } from '@stoplight/json'; import { get } from 'lodash'; import { resolveAlias } from '../../alias'; import { Formats } from '../../formats'; -import { wrapError } from './common/error'; +import { toParsedPath, wrapError } from './common/error'; +import { RulesetValidationError } from '../errors'; function getOverrides(overrides: unknown, key: string): Record | null { if (!Array.isArray(overrides)) return null; @@ -20,8 +21,9 @@ export function validateAlias( alias: string, path: string, ): Error | void { + const parsedPath = toParsedPath(path); + try { - const parsedPath = path.slice(1).split('/'); const formats: unknown = get(ruleset, [...parsedPath.slice(0, parsedPath.indexOf('rules') + 2), 'formats']); const aliases = @@ -34,6 +36,10 @@ export function validateAlias( resolveAlias(aliases ?? null, alias, Array.isArray(formats) ? new Formats(formats) : null); } catch (ex) { + if (ex instanceof ReferenceError) { + return new RulesetValidationError('undefined-alias', ex.message, parsedPath); + } + return wrapError(ex, path); } } diff --git a/packages/core/src/ruleset/validation/validators/common/error.ts b/packages/core/src/ruleset/validation/validators/common/error.ts index 214065984..278eb2cce 100644 --- a/packages/core/src/ruleset/validation/validators/common/error.ts +++ b/packages/core/src/ruleset/validation/validators/common/error.ts @@ -10,11 +10,11 @@ function toRulesetValidationError(this: ReadonlyArray, ex: unknown): Rul return ex; } - return new RulesetValidationError(isError(ex) ? ex.message : String(ex), [...this]); + return new RulesetValidationError('generic-validation-error', isError(ex) ? ex.message : String(ex), [...this]); } export function wrapError(ex: unknown, path: string): Error { - const parsedPath = path.slice(1).split('/'); + const parsedPath = toParsedPath(path); if (isAggregateError(ex)) { return new AggregateError(ex.errors.map(toRulesetValidationError, parsedPath)); @@ -22,3 +22,7 @@ export function wrapError(ex: unknown, path: string): Error { return toRulesetValidationError.call(parsedPath, ex); } + +export function toParsedPath(path: string): string[] { + return path.slice(1).split('/'); +} diff --git a/packages/core/src/ruleset/validation/validators/function.ts b/packages/core/src/ruleset/validation/validators/function.ts index 2c558d990..834425227 100644 --- a/packages/core/src/ruleset/validation/validators/function.ts +++ b/packages/core/src/ruleset/validation/validators/function.ts @@ -1,11 +1,12 @@ import type { RulesetFunction, RulesetFunctionWithValidator } from '../../../types'; -import { wrapError } from './common/error'; +import { toParsedPath, wrapError } from './common/error'; +import { RulesetValidationError } from '../errors'; function assertRulesetFunction( maybeRulesetFunction: unknown, ): asserts maybeRulesetFunction is RulesetFunction | RulesetFunctionWithValidator { if (typeof maybeRulesetFunction !== 'function') { - throw Error('Function is not defined'); + throw ReferenceError('Function is not defined'); } } @@ -22,6 +23,10 @@ export function validateFunction( const validator: RulesetFunctionWithValidator['validator'] = fn.validator.bind(fn); validator(opts); } catch (ex) { + if (ex instanceof ReferenceError) { + return new RulesetValidationError('undefined-function', ex.message, toParsedPath(path)); + } + return wrapError(ex, path); } }