lintPostcssResult.mjs 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  1. import { EOL } from 'node:os';
  2. import { DEFAULT_SEVERITY, RULE_NAME_ALL } from './constants.mjs';
  3. import { DEFAULT_CONFIGURATION_COMMENT } from './utils/configurationComment.mjs';
  4. import assignDisabledRanges from './assignDisabledRanges.mjs';
  5. import { emitDeprecationWarning } from './utils/emitWarning.mjs';
  6. import { fork } from 'css-tree';
  7. import getStylelintRule from './utils/getStylelintRule.mjs';
  8. import reportUnknownRuleNames from './reportUnknownRuleNames.mjs';
  9. import rules from './rules/index.mjs';
  10. import timing from './timing.mjs';
  11. /** @import {Config, LinterOptions, PostcssResult} from 'stylelint' */
  12. /**
  13. * @param {LinterOptions} stylelintOptions
  14. * @param {PostcssResult} postcssResult
  15. * @param {Config} config
  16. * @returns {Promise<any>}
  17. */
  18. export default async function lintPostcssResult(stylelintOptions, postcssResult, config) {
  19. postcssResult.stylelint.stylelintError = false;
  20. postcssResult.stylelint.stylelintWarning = false;
  21. postcssResult.stylelint.quiet = config.quiet;
  22. postcssResult.stylelint.quietDeprecationWarnings = stylelintOptions.quietDeprecationWarnings;
  23. postcssResult.stylelint.config = config;
  24. const postcssDoc = postcssResult.root;
  25. if (!('type' in postcssDoc)) {
  26. throw new Error('Unexpected Postcss root object!');
  27. }
  28. const newlineMatch = postcssDoc.source?.input.css.match(/\r?\n/);
  29. const newline = newlineMatch ? newlineMatch[0] : EOL;
  30. const configurationComment = config.configurationComment || DEFAULT_CONFIGURATION_COMMENT;
  31. const ctx = { configurationComment, newline };
  32. assignDisabledRanges(postcssDoc, postcssResult);
  33. const postcssRoots = /** @type {import('postcss').Root[]} */ (
  34. postcssDoc && postcssDoc.constructor.name === 'Document' ? postcssDoc.nodes : [postcssDoc]
  35. );
  36. // Promises for the rules. Although the rule code runs synchronously now,
  37. // the use of Promises makes it compatible with the possibility of async
  38. // rules down the line.
  39. /** @type {Array<Promise<any>>} */
  40. const performRules = [];
  41. const rulesOrder = Object.keys(rules);
  42. const ruleNames = config.rules
  43. ? Object.keys(config.rules).sort((a, b) => rulesOrder.indexOf(a) - rulesOrder.indexOf(b))
  44. : [];
  45. for (const ruleName of ruleNames) {
  46. const ruleFunction = await getStylelintRule(ruleName, config);
  47. if (ruleFunction === undefined) {
  48. performRules.push(
  49. Promise.all(
  50. postcssRoots.map((postcssRoot) =>
  51. reportUnknownRuleNames(ruleName, postcssRoot, postcssResult),
  52. ),
  53. ),
  54. );
  55. continue;
  56. }
  57. const ruleSettings = config.rules?.[ruleName];
  58. if (ruleSettings === null || ruleSettings[0] === null) continue;
  59. if (ruleFunction.meta?.deprecated && !stylelintOptions.quietDeprecationWarnings) {
  60. warnDeprecatedRule(postcssResult, ruleName);
  61. }
  62. const primaryOption = ruleSettings[0];
  63. const secondaryOptions = ruleSettings[1];
  64. // Log the rule's severity in the PostCSS result
  65. const defaultSeverity = config.defaultSeverity || DEFAULT_SEVERITY;
  66. postcssResult.stylelint.ruleSeverities[ruleName] =
  67. (secondaryOptions && secondaryOptions.severity) || defaultSeverity;
  68. postcssResult.stylelint.customMessages[ruleName] = secondaryOptions && secondaryOptions.message;
  69. postcssResult.stylelint.customUrls[ruleName] = secondaryOptions && secondaryOptions.url;
  70. postcssResult.stylelint.ruleMetadata[ruleName] = ruleFunction.meta || {};
  71. const shouldWarn = ruleFunction.meta?.fixable && !stylelintOptions.quietDeprecationWarnings;
  72. const disableFix = secondaryOptions?.disableFix === true;
  73. const fix = !disableFix && config.fix && isFixCompatible(postcssResult, ruleName);
  74. const lexer = getCachedLexer(config);
  75. const context = {
  76. ...ctx,
  77. lexer,
  78. // context.fix is unlikely to be removed in the foreseeable future
  79. // due to the sheer number of rules in the wild that rely on it
  80. get fix() {
  81. if (shouldWarn) {
  82. emitDeprecationWarning(
  83. '`context.fix` is being deprecated.',
  84. 'CONTEXT_FIX',
  85. `Please pass a \`fix\` callback to the \`report\` utility of "${ruleName}" instead.`,
  86. );
  87. }
  88. return fix;
  89. },
  90. };
  91. const ruleFn = ruleFunction(primaryOption, secondaryOptions, context);
  92. /**
  93. * @param {import('postcss').Root} postcssRoot
  94. */
  95. async function runRule(postcssRoot) {
  96. if (timing.enabled) {
  97. return timing.time(ruleName, () => ruleFn(postcssRoot, postcssResult))();
  98. }
  99. return ruleFn(postcssRoot, postcssResult);
  100. }
  101. performRules.push(Promise.all(postcssRoots.map(runRule)));
  102. }
  103. return Promise.all(performRules);
  104. }
  105. /**
  106. * using context.fix instead of the fix callback has the drawback
  107. * of not honouring the configuration comments in subtle ways
  108. * @see file://./../docs/user-guide/options.md#fix for details
  109. * @param {PostcssResult} postcssResult
  110. * @param {string} name
  111. * @returns {boolean}
  112. */
  113. function isFixCompatible({ stylelint: { disabledRanges } }, name) {
  114. return !disabledRanges[RULE_NAME_ALL]?.length && !disabledRanges[name];
  115. }
  116. /**
  117. * @param {PostcssResult} result
  118. * @param {string} ruleName
  119. * @returns {void}
  120. */
  121. function warnDeprecatedRule(result, ruleName) {
  122. const message = `The "${ruleName}" rule is deprecated.`;
  123. emitDeprecationWarning(
  124. message,
  125. 'RULE',
  126. `Please be aware that the "${ruleName}" rule will soon be either removed or renamed.`,
  127. );
  128. result.warn(message, { stylelintType: 'deprecation' });
  129. }
  130. const lexerCache = new Map();
  131. /**
  132. * @param {Config} config
  133. * @returns {import('css-tree').Lexer}
  134. * */
  135. function getCachedLexer(config) {
  136. const cacheKey = JSON.stringify(config.languageOptions?.syntax || {});
  137. if (lexerCache.has(cacheKey)) {
  138. return lexerCache.get(cacheKey);
  139. }
  140. const newLexer = fork({
  141. atrules: config.languageOptions?.syntax?.atRules || {},
  142. properties: config.languageOptions?.syntax?.properties || {},
  143. types: config.languageOptions?.syntax?.types || {},
  144. cssWideKeywords: config.languageOptions?.syntax?.cssWideKeywords || [],
  145. }).lexer;
  146. lexerCache.set(cacheKey, newLexer);
  147. return newLexer;
  148. }