diff --git a/packages/plugin-stylelint/src/lib/runner/model.ts b/packages/plugin-stylelint/src/lib/runner/model.ts index c472f13ab..6aede63b7 100644 --- a/packages/plugin-stylelint/src/lib/runner/model.ts +++ b/packages/plugin-stylelint/src/lib/runner/model.ts @@ -1,3 +1,16 @@ +import type { ConfigRuleSettings, Primary, Severity } from 'stylelint'; + +// Typing resource https://stylelint.io/user-guide/configure/ +/** Config rule setting of Stylelint excluding null and undefined values */ +export type ActiveConfigRuleSetting = Exclude< + ConfigRuleSettings>, + null | undefined +>; + +/** Output of the `getNormalizedConfigForFile` function. Config file of Stylelint */ export type NormalizedStyleLintConfig = { - config: { rules: Record }; + config: { + rules: Record>>; + defaultSeverity?: Severity; + }; }; diff --git a/packages/plugin-stylelint/src/lib/runner/normalize-config.ts b/packages/plugin-stylelint/src/lib/runner/normalize-config.ts index 8908a720b..d4ca45b04 100644 --- a/packages/plugin-stylelint/src/lib/runner/normalize-config.ts +++ b/packages/plugin-stylelint/src/lib/runner/normalize-config.ts @@ -10,7 +10,7 @@ export function getNormalizedConfigForFile({ cwd, }: Required> & { cwd?: string; -}) { +}): Promise { const _linter = stylelint._createLinter({ configFile: stylelintrc }); const configFile = stylelintrc ?? path.join(cwd ?? process.cwd(), '.stylelintrc.json'); diff --git a/packages/plugin-stylelint/src/lib/runner/utils.ts b/packages/plugin-stylelint/src/lib/runner/utils.ts index ed49add9e..2d8e49d25 100644 --- a/packages/plugin-stylelint/src/lib/runner/utils.ts +++ b/packages/plugin-stylelint/src/lib/runner/utils.ts @@ -1,5 +1,6 @@ -import type { ConfigRuleSettings, LintResult, Warning } from 'stylelint'; +import type { LintResult, Secondary, Severity, Warning } from 'stylelint'; import type { Audit, AuditOutputs, AuditReport } from '@code-pushup/models'; +import type { ActiveConfigRuleSetting } from './model.js'; export function mapStylelintResultsToAudits( results: LintResult[], @@ -73,24 +74,39 @@ export function getSeverityFromWarning(warning: Warning): 'error' | 'warning' { throw new Error(`Unknown severity: ${severity}`); } +/** + * Function that returns the severity from a ruleConfig. + * If the ruleConfig is not an array, the default severity of the config file must be returned, since the custom severity can be only specified in an array. + * If the ruleConfig is an array, a custom severity might have been set, in that case, it must be returned + * @param ruleConfig - The Stylelint rule config value + * @param defaultSeverity - The default severity of the config file. By default, it's 'error' + * @returns The severity (EX: 'error' | 'warning') + */ export function getSeverityFromRuleConfig( - ruleConfig: ConfigRuleSettings, -): 'error' | 'warning' { - if (!ruleConfig) { - // Default severity if the ruleConfig is null or undefined - return 'error'; + ruleConfig: ActiveConfigRuleSetting, + defaultSeverity: Severity = 'error', +): Severity { + //If it's not an array, the default severity of the config file must be returned, since the custom severity can be only specified in an array. + if (!Array.isArray(ruleConfig)) { + return defaultSeverity; } - if (Array.isArray(ruleConfig)) { - const options = ruleConfig[1]; - if (options && typeof options === 'object' && 'severity' in options) { - const severity = options.severity; - if (severity === 'warning') { - return 'warning'; - } - } + // If it's an array, a custom severity might have been set, in that case, it must be returned + + const secondary: Secondary = ruleConfig.at(1); + + if (secondary == null) { + return defaultSeverity; + } + + if (!secondary['severity']) { + return defaultSeverity; + } + + if (typeof secondary['severity'] === 'function') { + console.warn('Function severity is not supported'); + return defaultSeverity; } - // Default severity if severity is not explicitly set - return 'error'; + return secondary['severity']; } diff --git a/packages/plugin-stylelint/src/lib/runner/utils.unit.test.ts b/packages/plugin-stylelint/src/lib/runner/utils.unit.test.ts new file mode 100644 index 000000000..0e79f4626 --- /dev/null +++ b/packages/plugin-stylelint/src/lib/runner/utils.unit.test.ts @@ -0,0 +1,54 @@ +import { describe, expect, it } from 'vitest'; +import type { ActiveConfigRuleSetting } from './model.js'; +import { getSeverityFromRuleConfig } from './utils.js'; + +describe('getSeverityFromRuleConfig', () => { + it('should respect the default severity when from the default', () => { + expect(getSeverityFromRuleConfig([true])).toBe('error'); + }); + + it('should consider the default severity when its different from the default', () => { + expect(getSeverityFromRuleConfig([true], 'warning')).toBe('warning'); + }); + + it.each([true, 5, 'percentage', ['/\\[.+]/', 'percentage'], { a: 1 }])( + 'should return the default severity for a primary value %s', + ruleConfig => { + expect( + getSeverityFromRuleConfig(ruleConfig as ActiveConfigRuleSetting), + ).toBe('error'); + }, + ); + + it('should return the default severity when the rule config does not have a secondary item', () => { + expect(getSeverityFromRuleConfig([true])).toBe('error'); + }); + + it('should return the default severity when the secondary item is missing the `severity` property', () => { + expect(getSeverityFromRuleConfig([true, {}])).toBe('error'); + }); + + it('should return the default severity when `severity` property is of type function', () => { + expect(getSeverityFromRuleConfig([true, { severity: () => {} }])).toBe( + 'error', + ); + }); + + it.each([ + { ruleConfig: [true, { severity: 'warning' }], expected: 'warning' }, + { ruleConfig: [true, { severity: 'error' }], expected: 'error' }, + ])('should return the set severity `%s`', ({ ruleConfig, expected }) => { + expect(getSeverityFromRuleConfig(ruleConfig)).toBe(expected); + }); + + it.each([null, undefined])( + 'should return the default severity for disabled rules %s', + ruleConfig => { + expect( + getSeverityFromRuleConfig( + ruleConfig as unknown as ActiveConfigRuleSetting, + ), + ).toBe('error'); + }, + ); +}); diff --git a/packages/plugin-stylelint/src/lib/utils.ts b/packages/plugin-stylelint/src/lib/utils.ts index a96e2af26..875a7bc1e 100644 --- a/packages/plugin-stylelint/src/lib/utils.ts +++ b/packages/plugin-stylelint/src/lib/utils.ts @@ -1,3 +1,4 @@ +import type { ConfigRuleSettings } from 'stylelint'; import type { Audit, CategoryRef } from '@code-pushup/models'; import { type StyleLintPluginConfig, @@ -9,6 +10,7 @@ import { GROUPS, STYLELINT_PLUGIN_SLUG, } from './constants.js'; +import type { ActiveConfigRuleSetting } from './runner/model.js'; import { getNormalizedConfigForFile } from './runner/normalize-config.js'; import { getSeverityFromRuleConfig } from './runner/utils.js'; @@ -31,12 +33,16 @@ export async function getGroups( options: Required>, ) { const { config } = await getNormalizedConfigForFile(options); - const { rules } = config; + const { rules, defaultSeverity } = config; return GROUPS.map(group => ({ ...group, refs: Object.entries(rules) + .filter(filterNonNull) // TODO Type Narrowing is not fully working for the nulls / undefineds .filter(([_, ruleConfig]) => { - const severity = getSeverityFromRuleConfig(ruleConfig); + const severity = getSeverityFromRuleConfig( + ruleConfig as ActiveConfigRuleSetting, + defaultSeverity, + ); if (severity === 'error' && group.slug === 'problems') { return true; } else if (severity === 'warning' && group.slug === 'suggestions') { @@ -75,3 +81,12 @@ export async function getCategoryRefsFromAudits( type: 'audit', })); } + +function filterNonNull( + settings: Array>, +): Exclude, null | undefined>[] { + return settings.filter( + (setting): setting is Exclude, null | undefined> => + setting !== null && setting !== undefined, + ); +}