Skip to content

Commit 378b4b8

Browse files
authored
fix(core): improve deep ruleset inheritance (stoplightio#2326)
1 parent da33ad6 commit 378b4b8

File tree

4 files changed

+53
-5
lines changed

4 files changed

+53
-5
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { DiagnosticSeverity } from '@stoplight/types';
2+
import { truthy } from '@stoplight/spectral-functions';
3+
4+
const ruleset1 = {
5+
rules: {
6+
'custom-info-description': {
7+
message: 'API Description is missing',
8+
severity: DiagnosticSeverity.Error,
9+
given: '$.info',
10+
then: {
11+
field: 'description',
12+
function: truthy,
13+
},
14+
},
15+
},
16+
};
17+
18+
const ruleset2 = {
19+
extends: [ruleset1],
20+
rules: {},
21+
};
22+
23+
export default {
24+
extends: [[ruleset2, 'off']],
25+
rules: {},
26+
};

packages/core/src/ruleset/__tests__/ruleset.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,13 @@ describe('Ruleset', () => {
9595
expect(getEnabledRules(rules)).toEqual(['overridable-rule']);
9696
});
9797

98+
it('given nested extends with severity set to off #2', async () => {
99+
const { rules } = await loadRuleset(import('./__fixtures__/severity/off-proxy2'));
100+
expect(Object.keys(rules)).toEqual(['custom-info-description']);
101+
102+
expect(getEnabledRules(rules)).toEqual([]);
103+
});
104+
98105
it('given nested extends with severity set to off and explicit override to error', async () => {
99106
const { rules } = await loadRuleset(import('./__fixtures__/severity/error'));
100107
expect(Object.keys(rules)).toEqual([

packages/core/src/ruleset/rule.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import type { HumanReadableDiagnosticSeverity, IRuleThen, RuleDefinition, String
1111
import { minimatch } from './utils/minimatch';
1212
import { Formats } from './formats';
1313
import { resolveAlias } from './alias';
14-
import type { Stringified } from './types';
14+
import type { Stringified, FileRulesetSeverityDefinition } from './types';
1515

1616
export interface IRule {
1717
description: string | null;
@@ -73,6 +73,10 @@ export class Rule implements IRule {
7373
this.#enabled = enabled;
7474
}
7575

76+
public static isEnabled(rule: IRule, severity: FileRulesetSeverityDefinition): boolean {
77+
return severity === 'all' || (severity === 'recommended' && rule.recommended);
78+
}
79+
7680
public getSeverityForSource(source: string, path: JsonPath): DiagnosticSeverity | -1 {
7781
if (this.overrides === void 0 || this.overrides.definition.size === 0) {
7882
return this.severity;

packages/core/src/ruleset/ruleset.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,14 @@ import { isSimpleAliasDefinition } from './utils/guards';
2020
import type { Stringified } from './types';
2121

2222
const STACK_SYMBOL = Symbol('@stoplight/spectral/ruleset/#stack');
23+
const EXPLICIT_SEVERITY = Symbol('@stoplight/spectral/ruleset/#explicit-severity');
2324
const DEFAULT_RULESET_FILE = /^\.?spectral\.(ya?ml|json|m?js)$/;
2425

2526
type RulesetContext = {
2627
readonly severity?: FileRulesetSeverityDefinition;
2728
readonly source?: string;
2829
readonly [STACK_SYMBOL]?: Map<RulesetDefinition, Ruleset>;
30+
readonly [EXPLICIT_SEVERITY]?: boolean;
2931
};
3032

3133
let SEED = 1;
@@ -110,8 +112,9 @@ export class Ruleset {
110112
(extensions, extension) => {
111113
let actualExtension;
112114
let severity: FileRulesetSeverityDefinition = 'recommended';
115+
const explicitSeverity = Array.isArray(extension);
113116

114-
if (Array.isArray(extension)) {
117+
if (explicitSeverity) {
115118
[actualExtension, severity] = extension;
116119
} else {
117120
actualExtension = extension;
@@ -123,7 +126,13 @@ export class Ruleset {
123126
return extensions;
124127
}
125128

126-
extensions.push(new Ruleset(actualExtension, { severity, [STACK_SYMBOL]: stack }));
129+
extensions.push(
130+
new Ruleset(actualExtension, {
131+
severity,
132+
[STACK_SYMBOL]: stack,
133+
[EXPLICIT_SEVERITY]: explicitSeverity,
134+
}),
135+
);
127136
return extensions;
128137
},
129138
[],
@@ -271,6 +280,9 @@ export class Ruleset {
271280
if (extendedRuleset === this) continue;
272281
for (const rule of Object.values(extendedRuleset.rules)) {
273282
rules[rule.name] = rule;
283+
if (this.#context[STACK_SYMBOL] !== void 0 && this.#context[EXPLICIT_SEVERITY] === true) {
284+
rule.enabled = Rule.isEnabled(rule, this.#context.severity);
285+
}
274286
}
275287
}
276288
}
@@ -281,8 +293,7 @@ export class Ruleset {
281293
rules[name] = rule;
282294

283295
if (rule.owner === this) {
284-
rule.enabled =
285-
this.#context.severity === 'all' || (this.#context.severity === 'recommended' && rule.recommended);
296+
rule.enabled = Rule.isEnabled(rule, this.#context.severity);
286297
}
287298

288299
if (rule.formats !== null) {

0 commit comments

Comments
 (0)