diff --git a/packages/core/src/ruleset/__tests__/__fixtures__/severity/off-proxy2.ts b/packages/core/src/ruleset/__tests__/__fixtures__/severity/off-proxy2.ts new file mode 100644 index 000000000..cd52917a5 --- /dev/null +++ b/packages/core/src/ruleset/__tests__/__fixtures__/severity/off-proxy2.ts @@ -0,0 +1,26 @@ +import { DiagnosticSeverity } from '@stoplight/types'; +import { truthy } from '@stoplight/spectral-functions'; + +const ruleset1 = { + rules: { + 'custom-info-description': { + message: 'API Description is missing', + severity: DiagnosticSeverity.Error, + given: '$.info', + then: { + field: 'description', + function: truthy, + }, + }, + }, +}; + +const ruleset2 = { + extends: [ruleset1], + rules: {}, +}; + +export default { + extends: [[ruleset2, 'off']], + rules: {}, +}; diff --git a/packages/core/src/ruleset/__tests__/ruleset.test.ts b/packages/core/src/ruleset/__tests__/ruleset.test.ts index 7068adb21..d509cf966 100644 --- a/packages/core/src/ruleset/__tests__/ruleset.test.ts +++ b/packages/core/src/ruleset/__tests__/ruleset.test.ts @@ -95,6 +95,13 @@ describe('Ruleset', () => { expect(getEnabledRules(rules)).toEqual(['overridable-rule']); }); + it('given nested extends with severity set to off #2', async () => { + const { rules } = await loadRuleset(import('./__fixtures__/severity/off-proxy2')); + expect(Object.keys(rules)).toEqual(['custom-info-description']); + + expect(getEnabledRules(rules)).toEqual([]); + }); + it('given nested extends with severity set to off and explicit override to error', async () => { const { rules } = await loadRuleset(import('./__fixtures__/severity/error')); expect(Object.keys(rules)).toEqual([ diff --git a/packages/core/src/ruleset/rule.ts b/packages/core/src/ruleset/rule.ts index c0e2b6f83..6ce9aa850 100644 --- a/packages/core/src/ruleset/rule.ts +++ b/packages/core/src/ruleset/rule.ts @@ -11,7 +11,7 @@ import type { HumanReadableDiagnosticSeverity, IRuleThen, RuleDefinition, String import { minimatch } from './utils/minimatch'; import { Formats } from './formats'; import { resolveAlias } from './alias'; -import type { Stringified } from './types'; +import type { Stringified, FileRulesetSeverityDefinition } from './types'; export interface IRule { description: string | null; @@ -73,6 +73,10 @@ export class Rule implements IRule { this.#enabled = enabled; } + public static isEnabled(rule: IRule, severity: FileRulesetSeverityDefinition): boolean { + return severity === 'all' || (severity === 'recommended' && rule.recommended); + } + public getSeverityForSource(source: string, path: JsonPath): DiagnosticSeverity | -1 { if (this.overrides === void 0 || this.overrides.definition.size === 0) { return this.severity; diff --git a/packages/core/src/ruleset/ruleset.ts b/packages/core/src/ruleset/ruleset.ts index 4020b37e0..560957485 100644 --- a/packages/core/src/ruleset/ruleset.ts +++ b/packages/core/src/ruleset/ruleset.ts @@ -20,12 +20,14 @@ import { isSimpleAliasDefinition } from './utils/guards'; import type { Stringified } from './types'; const STACK_SYMBOL = Symbol('@stoplight/spectral/ruleset/#stack'); +const EXPLICIT_SEVERITY = Symbol('@stoplight/spectral/ruleset/#explicit-severity'); const DEFAULT_RULESET_FILE = /^\.?spectral\.(ya?ml|json|m?js)$/; type RulesetContext = { readonly severity?: FileRulesetSeverityDefinition; readonly source?: string; readonly [STACK_SYMBOL]?: Map; + readonly [EXPLICIT_SEVERITY]?: boolean; }; let SEED = 1; @@ -110,8 +112,9 @@ export class Ruleset { (extensions, extension) => { let actualExtension; let severity: FileRulesetSeverityDefinition = 'recommended'; + const explicitSeverity = Array.isArray(extension); - if (Array.isArray(extension)) { + if (explicitSeverity) { [actualExtension, severity] = extension; } else { actualExtension = extension; @@ -123,7 +126,13 @@ export class Ruleset { return extensions; } - extensions.push(new Ruleset(actualExtension, { severity, [STACK_SYMBOL]: stack })); + extensions.push( + new Ruleset(actualExtension, { + severity, + [STACK_SYMBOL]: stack, + [EXPLICIT_SEVERITY]: explicitSeverity, + }), + ); return extensions; }, [], @@ -271,6 +280,9 @@ export class Ruleset { if (extendedRuleset === this) continue; for (const rule of Object.values(extendedRuleset.rules)) { rules[rule.name] = rule; + if (this.#context[STACK_SYMBOL] !== void 0 && this.#context[EXPLICIT_SEVERITY] === true) { + rule.enabled = Rule.isEnabled(rule, this.#context.severity); + } } } } @@ -281,8 +293,7 @@ export class Ruleset { rules[name] = rule; if (rule.owner === this) { - rule.enabled = - this.#context.severity === 'all' || (this.#context.severity === 'recommended' && rule.recommended); + rule.enabled = Rule.isEnabled(rule, this.#context.severity); } if (rule.formats !== null) {