Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { truthy } from '@stoplight/spectral-functions';

// Test case 1: Deep nesting (great-grandparent chain)
// great-grandparent -> grandparent (enables) -> parent (enables) -> child (off)

const greatGrandparent = {
rules: {
'my-rule': {
given: '$',
then: { function: truthy },
},
},
};

const grandparent = {
extends: [[greatGrandparent, 'off']],
rules: {
'my-rule': true,
},
};

const parent = {
extends: [[grandparent, 'off']],
rules: {
'my-rule': true,
},
};

export default {
extends: [[parent, 'off']],
rules: {},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { truthy } from '@stoplight/spectral-functions';

// Test case 2: Mixed severity modifiers followed by off
// grandparent -> parent (off, re-enables with 'warn') -> child (off)

const grandparent = {
rules: {
'my-rule': {
given: '$',
then: { function: truthy },
},
},
};

const parent = {
extends: [[grandparent, 'off']],
rules: {
'my-rule': 'warn',
},
};

export default {
extends: [[parent, 'off']],
rules: {},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { truthy } from '@stoplight/spectral-functions';

// Test case 4: Non-recommended rule enabled then off
// grandparent (recommended:false) -> parent (enables with true) -> child (off)

const grandparent = {
rules: {
'my-rule': {
given: '$',
recommended: false,
then: { function: truthy },
},
},
};

const parent = {
extends: [grandparent],
rules: {
'my-rule': true,
},
};

export default {
extends: [[parent, 'off']],
rules: {},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { truthy } from '@stoplight/spectral-functions';

// Test case 3: Object rule modification followed by off
// grandparent -> parent (modifies rule with object) -> child (off)

const grandparent = {
rules: {
'my-rule': {
given: '$',
then: { function: truthy },
},
},
};

const parent = {
extends: [[grandparent, 'off']],
rules: {
'my-rule': {
given: '$.info',
then: { function: truthy },
},
},
};

export default {
extends: [[parent, 'off']],
rules: {},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { truthy } from '@stoplight/spectral-functions';
import type { RulesetDefinition } from '@stoplight/spectral-core';

// Test case 6: Child re-enables rule after extending with off
// grandparent -> parent -> child (extends with 'off', but re-enables my-rule with true)

const grandparent: RulesetDefinition = {
rules: {
'my-rule': {
given: '$',
then: { function: truthy },
},
'my-rule-2': {
given: '$',
then: { function: truthy },
},
},
};

const parent: RulesetDefinition = {
extends: [grandparent],
rules: {},
};

export default {
extends: [[parent, 'off']],
rules: {
'my-rule': true,
},
} as RulesetDefinition;
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { truthy } from '@stoplight/spectral-functions';

// Test case: nested extends with 'off' severity and intermediate rule enable
// The key scenario is: grandparent -> parent (enables rule) -> child (extends with 'off')

const grandparent = {
rules: {
'my-rule': {
given: '$',
then: { function: truthy },
},
'my-rule-2': {
given: '$',
severity: 'error',
then: { function: truthy },
},
'my-rule-3': {
given: '$',
severity: 'error',
then: { function: truthy },
},
},
};

const parent = {
extends: [[grandparent, 'off']],
rules: {
'my-rule': true,
'my-rule-2': false,
'my-rule-3': 'warn',
},
};

export default {
extends: [[parent, 'off']],
rules: {},
};
49 changes: 49 additions & 0 deletions packages/core/src/ruleset/__tests__/ruleset.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,55 @@ describe('Ruleset', () => {
expect(getEnabledRules(rules)).toEqual([]);
});

it('given nested extends with severity set to off and intermediate rule enable', async () => {
const { rules } = await loadRuleset(import('./__fixtures__/severity/off-with-intermediate-enable'));
expect(Object.keys(rules)).toEqual(['my-rule', 'my-rule-2', 'my-rule-3']);

// parent re-enables my-rule and my-rule-3, but child extends with 'off' so all should be disabled
expect(getEnabledRules(rules)).toEqual([]);
});

it('given deep nesting with off at the end', async () => {
const { rules } = await loadRuleset(import('./__fixtures__/severity/off-deep-nesting'));
expect(Object.keys(rules)).toEqual(['my-rule']);

// great-grandparent -> grandparent (enables) -> parent (enables) -> child (off)
// All should be disabled
expect(getEnabledRules(rules)).toEqual([]);
});

it('given mixed severity modifiers followed by off', async () => {
const { rules } = await loadRuleset(import('./__fixtures__/severity/off-mixed-severities'));
expect(Object.keys(rules)).toEqual(['my-rule']);

// parent sets severity to 'warn', child extends with 'off' - should be disabled
expect(getEnabledRules(rules)).toEqual([]);
});

it('given object rule modification followed by off', async () => {
const { rules } = await loadRuleset(import('./__fixtures__/severity/off-object-modification'));
expect(Object.keys(rules)).toEqual(['my-rule']);

// parent modifies rule with object, child extends with 'off' - should be disabled
expect(getEnabledRules(rules)).toEqual([]);
});

it('given non-recommended rule enabled then off', async () => {
const { rules } = await loadRuleset(import('./__fixtures__/severity/off-non-recommended'));
expect(Object.keys(rules)).toEqual(['my-rule']);

// grandparent has recommended:false, parent enables with true, child extends with 'off'
expect(getEnabledRules(rules)).toEqual([]);
});

it('given child re-enables rule after extending with off', async () => {
const { rules } = await loadRuleset(import('./__fixtures__/severity/off-then-reenable'));
expect(Object.keys(rules)).toEqual(['my-rule', 'my-rule-2']);

// parent has rules, child extends with 'off' but explicitly re-enables my-rule
expect(getEnabledRules(rules)).toEqual(['my-rule']);
});

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([
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/ruleset/ruleset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,8 @@ export class Ruleset {
const rule = mergeRule(rules[name], name, definition, this);
rules[name] = rule;

if (rule.owner === this) {
// When extending with explicit severity (e.g., "off"), apply it to modified rules too
if (rule.owner === this || this.#context[EXPLICIT_SEVERITY] === true) {
rule.enabled = Rule.isEnabled(rule, this.#context.severity);
}

Expand Down