import { EOL } from 'node:os'; import { DEFAULT_SEVERITY, RULE_NAME_ALL } from './constants.mjs'; import { DEFAULT_CONFIGURATION_COMMENT } from './utils/configurationComment.mjs'; import assignDisabledRanges from './assignDisabledRanges.mjs'; import emitDeprecationWarning from './utils/emitDeprecationWarning.mjs'; import { fork } from 'css-tree'; import getStylelintRule from './utils/getStylelintRule.mjs'; import reportUnknownRuleNames from './reportUnknownRuleNames.mjs'; import rules from './rules/index.mjs'; import timing from './timing.mjs'; /** @import {Config, LinterOptions, PostcssResult} from 'stylelint' */ /** * @param {LinterOptions} stylelintOptions * @param {PostcssResult} postcssResult * @param {Config} config * @returns {Promise} */ export default async function lintPostcssResult(stylelintOptions, postcssResult, config) { postcssResult.stylelint.stylelintError = false; postcssResult.stylelint.stylelintWarning = false; postcssResult.stylelint.quiet = config.quiet; postcssResult.stylelint.quietDeprecationWarnings = stylelintOptions.quietDeprecationWarnings; postcssResult.stylelint.config = config; const postcssDoc = postcssResult.root; if (!('type' in postcssDoc)) { throw new Error('Unexpected Postcss root object!'); } const newlineMatch = postcssDoc.source?.input.css.match(/\r?\n/); const newline = newlineMatch ? newlineMatch[0] : EOL; const configurationComment = config.configurationComment || DEFAULT_CONFIGURATION_COMMENT; const ctx = { configurationComment, newline }; assignDisabledRanges(postcssDoc, postcssResult); const postcssRoots = /** @type {import('postcss').Root[]} */ ( postcssDoc && postcssDoc.constructor.name === 'Document' ? postcssDoc.nodes : [postcssDoc] ); // Promises for the rules. Although the rule code runs synchronously now, // the use of Promises makes it compatible with the possibility of async // rules down the line. /** @type {Array>} */ const performRules = []; const rulesOrder = Object.keys(rules); const ruleNames = config.rules ? Object.keys(config.rules).sort((a, b) => rulesOrder.indexOf(a) - rulesOrder.indexOf(b)) : []; for (const ruleName of ruleNames) { const ruleFunction = await getStylelintRule(ruleName, config); if (ruleFunction === undefined) { performRules.push( Promise.all( postcssRoots.map((postcssRoot) => reportUnknownRuleNames(ruleName, postcssRoot, postcssResult), ), ), ); continue; } const ruleSettings = config.rules?.[ruleName]; if (ruleSettings === null || ruleSettings[0] === null) continue; if (ruleFunction.meta?.deprecated && !stylelintOptions.quietDeprecationWarnings) { warnDeprecatedRule(postcssResult, ruleName); } const primaryOption = ruleSettings[0]; const secondaryOptions = ruleSettings[1]; // Log the rule's severity in the PostCSS result const defaultSeverity = config.defaultSeverity || DEFAULT_SEVERITY; postcssResult.stylelint.ruleSeverities[ruleName] = (secondaryOptions && secondaryOptions.severity) || defaultSeverity; postcssResult.stylelint.customMessages[ruleName] = secondaryOptions && secondaryOptions.message; postcssResult.stylelint.customUrls[ruleName] = secondaryOptions && secondaryOptions.url; postcssResult.stylelint.ruleMetadata[ruleName] = ruleFunction.meta || {}; const shouldWarn = ruleFunction.meta?.fixable && !stylelintOptions.quietDeprecationWarnings; const disableFix = secondaryOptions?.disableFix === true; const fix = !disableFix && config.fix && isFixCompatible(postcssResult, ruleName); const lexer = getCachedLexer(config); const context = { ...ctx, lexer, // context.fix is unlikely to be removed in the foreseeable future // due to the sheer number of rules in the wild that rely on it get fix() { if (shouldWarn) { emitDeprecationWarning( '`context.fix` is being deprecated.', 'CONTEXT_FIX', `Please pass a \`fix\` callback to the \`report\` utility of "${ruleName}" instead.`, ); } return fix; }, }; const ruleFn = ruleFunction(primaryOption, secondaryOptions, context); /** * @param {import('postcss').Root} postcssRoot */ async function runRule(postcssRoot) { if (timing.enabled) { return timing.time(ruleName, () => ruleFn(postcssRoot, postcssResult))(); } return ruleFn(postcssRoot, postcssResult); } performRules.push(Promise.all(postcssRoots.map(runRule))); } return Promise.all(performRules); } /** * using context.fix instead of the fix callback has the drawback * of not honouring the configuration comments in subtle ways * @see file://./../docs/user-guide/options.md#fix for details * @param {PostcssResult} postcssResult * @param {string} name * @returns {boolean} */ function isFixCompatible({ stylelint: { disabledRanges } }, name) { return !disabledRanges[RULE_NAME_ALL]?.length && !disabledRanges[name]; } /** * @param {PostcssResult} result * @param {string} ruleName * @returns {void} */ function warnDeprecatedRule(result, ruleName) { const message = `The "${ruleName}" rule is deprecated.`; emitDeprecationWarning( message, 'RULE', `Please be aware that the "${ruleName}" rule will soon be either removed or renamed.`, ); result.warn(message, { stylelintType: 'deprecation' }); } const lexerCache = new Map(); /** * @param {Config} config * @returns {import('css-tree').Lexer} * */ function getCachedLexer(config) { const cacheKey = JSON.stringify(config.languageOptions?.syntax || {}); if (lexerCache.has(cacheKey)) { return lexerCache.get(cacheKey); } const newLexer = fork({ atrules: config.languageOptions?.syntax?.atRules || {}, properties: config.languageOptions?.syntax?.properties || {}, types: config.languageOptions?.syntax?.types || {}, cssWideKeywords: config.languageOptions?.syntax?.cssWideKeywords || [], }).lexer; lexerCache.set(cacheKey, newLexer); return newLexer; }