needlessDisables.mjs 2.8 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091
  1. import { RULE_NAME_ALL } from './constants.mjs';
  2. import optionsMatches from './utils/optionsMatches.mjs';
  3. import putIfAbsent from './utils/putIfAbsent.mjs';
  4. import reportCommentProblem from './utils/reportCommentProblem.mjs';
  5. import validateDisableSettings from './validateDisableSettings.mjs';
  6. /**
  7. * @param {import('stylelint').PostcssResult} postcssResult
  8. * @returns {void}
  9. */
  10. export default function needlessDisables(postcssResult) {
  11. const [enabled, options] = validateDisableSettings(postcssResult, 'reportNeedlessDisables');
  12. if (!options) return;
  13. const {
  14. disabledRanges,
  15. disabledRanges: { [RULE_NAME_ALL]: disabledRangesAll = [] },
  16. disabledWarnings = [],
  17. } = postcssResult.stylelint;
  18. // A map from `stylelint-disable` comments to the set of rules that
  19. // are usefully disabled by each comment. We track this
  20. // comment-by-comment rather than range-by-range because ranges that
  21. // disable *all* rules are duplicated for each rule they apply to in
  22. // practice.
  23. /** @type {Map<import('postcss').Node, Set<string>>}} */
  24. const usefulDisables = new Map();
  25. for (const warning of disabledWarnings) {
  26. const rule = warning.rule;
  27. const ruleRanges = disabledRanges[rule];
  28. if (ruleRanges) {
  29. for (const range of ruleRanges) {
  30. if (isWarningInRange(warning, range)) {
  31. putIfAbsent(usefulDisables, range.node, () => new Set()).add(rule);
  32. }
  33. }
  34. }
  35. for (const range of disabledRangesAll) {
  36. if (isWarningInRange(warning, range)) {
  37. putIfAbsent(usefulDisables, range.node, () => new Set()).add(rule);
  38. }
  39. }
  40. }
  41. const allRangeNodes = new Set(disabledRangesAll.map((range) => range.node));
  42. for (const [rule, ranges] of Object.entries(disabledRanges)) {
  43. for (const range of ranges) {
  44. const node = range.node;
  45. if (rule !== RULE_NAME_ALL && allRangeNodes.has(node)) continue;
  46. if (enabled === optionsMatches(options, 'except', rule)) continue;
  47. const useful = usefulDisables.get(node) || new Set();
  48. // Only emit a warning if this range's comment isn't useful for this rule.
  49. // For the special rule "all", only emit a warning if it's not useful for
  50. // *any* rules, because it covers all of them.
  51. if (rule === RULE_NAME_ALL ? useful.size !== 0 : useful.has(rule)) continue;
  52. reportCommentProblem({
  53. rule: '--report-needless-disables',
  54. message: `Needless disable for "${rule}"`,
  55. severity: options.severity,
  56. node,
  57. postcssResult,
  58. });
  59. }
  60. }
  61. }
  62. /**
  63. * @param {import('stylelint').DisabledWarning} warning
  64. * @param {import('stylelint').DisabledRange} range
  65. * @returns {boolean}
  66. */
  67. function isWarningInRange(warning, range) {
  68. const line = warning.line;
  69. // Need to check if range.end exist, because line number type cannot be compared to undefined
  70. return (
  71. range.start <= line &&
  72. ((range.end !== undefined && range.end >= line) || range.end === undefined)
  73. );
  74. }