apply-disable-directives.js 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. /**
  2. * @fileoverview A module that filters reported problems based on `eslint-disable` and `eslint-enable` comments
  3. * @author Teddy Katz
  4. */
  5. "use strict";
  6. const lodash = require("lodash");
  7. /**
  8. * Compares the locations of two objects in a source file
  9. * @param {{line: number, column: number}} itemA The first object
  10. * @param {{line: number, column: number}} itemB The second object
  11. * @returns {number} A value less than 1 if itemA appears before itemB in the source file, greater than 1 if
  12. * itemA appears after itemB in the source file, or 0 if itemA and itemB have the same location.
  13. */
  14. function compareLocations(itemA, itemB) {
  15. return itemA.line - itemB.line || itemA.column - itemB.column;
  16. }
  17. /**
  18. * This is the same as the exported function, except that it
  19. * doesn't handle disable-line and disable-next-line directives, and it always reports unused
  20. * disable directives.
  21. * @param {Object} options options for applying directives. This is the same as the options
  22. * for the exported function, except that `reportUnusedDisableDirectives` is not supported
  23. * (this function always reports unused disable directives).
  24. * @returns {{problems: Problem[], unusedDisableDirectives: Problem[]}} An object with a list
  25. * of filtered problems and unused eslint-disable directives
  26. */
  27. function applyDirectives(options) {
  28. const problems = [];
  29. let nextDirectiveIndex = 0;
  30. let currentGlobalDisableDirective = null;
  31. const disabledRuleMap = new Map();
  32. // enabledRules is only used when there is a current global disable directive.
  33. const enabledRules = new Set();
  34. const usedDisableDirectives = new Set();
  35. for (const problem of options.problems) {
  36. while (
  37. nextDirectiveIndex < options.directives.length &&
  38. compareLocations(options.directives[nextDirectiveIndex], problem) <= 0
  39. ) {
  40. const directive = options.directives[nextDirectiveIndex++];
  41. switch (directive.type) {
  42. case "disable":
  43. if (directive.ruleId === null) {
  44. currentGlobalDisableDirective = directive;
  45. disabledRuleMap.clear();
  46. enabledRules.clear();
  47. } else if (currentGlobalDisableDirective) {
  48. enabledRules.delete(directive.ruleId);
  49. disabledRuleMap.set(directive.ruleId, directive);
  50. } else {
  51. disabledRuleMap.set(directive.ruleId, directive);
  52. }
  53. break;
  54. case "enable":
  55. if (directive.ruleId === null) {
  56. currentGlobalDisableDirective = null;
  57. disabledRuleMap.clear();
  58. } else if (currentGlobalDisableDirective) {
  59. enabledRules.add(directive.ruleId);
  60. disabledRuleMap.delete(directive.ruleId);
  61. } else {
  62. disabledRuleMap.delete(directive.ruleId);
  63. }
  64. break;
  65. // no default
  66. }
  67. }
  68. if (disabledRuleMap.has(problem.ruleId)) {
  69. usedDisableDirectives.add(disabledRuleMap.get(problem.ruleId));
  70. } else if (currentGlobalDisableDirective && !enabledRules.has(problem.ruleId)) {
  71. usedDisableDirectives.add(currentGlobalDisableDirective);
  72. } else {
  73. problems.push(problem);
  74. }
  75. }
  76. const unusedDisableDirectives = options.directives
  77. .filter(directive => directive.type === "disable" && !usedDisableDirectives.has(directive))
  78. .map(directive => ({
  79. ruleId: null,
  80. message: directive.ruleId
  81. ? `Unused eslint-disable directive (no problems were reported from '${directive.ruleId}').`
  82. : "Unused eslint-disable directive (no problems were reported).",
  83. line: directive.unprocessedDirective.line,
  84. column: directive.unprocessedDirective.column,
  85. severity: options.reportUnusedDisableDirectives === "warn" ? 1 : 2,
  86. nodeType: null
  87. }));
  88. return { problems, unusedDisableDirectives };
  89. }
  90. /**
  91. * Given a list of directive comments (i.e. metadata about eslint-disable and eslint-enable comments) and a list
  92. * of reported problems, determines which problems should be reported.
  93. * @param {Object} options Information about directives and problems
  94. * @param {{
  95. * type: ("disable"|"enable"|"disable-line"|"disable-next-line"),
  96. * ruleId: (string|null),
  97. * line: number,
  98. * column: number
  99. * }} options.directives Directive comments found in the file, with one-based columns.
  100. * Two directive comments can only have the same location if they also have the same type (e.g. a single eslint-disable
  101. * comment for two different rules is represented as two directives).
  102. * @param {{ruleId: (string|null), line: number, column: number}[]} options.problems
  103. * A list of problems reported by rules, sorted by increasing location in the file, with one-based columns.
  104. * @param {"off" | "warn" | "error"} options.reportUnusedDisableDirectives If `"warn"` or `"error"`, adds additional problems for unused directives
  105. * @returns {{ruleId: (string|null), line: number, column: number}[]}
  106. * A list of reported problems that were not disabled by the directive comments.
  107. */
  108. module.exports = ({ directives, problems, reportUnusedDisableDirectives = "off" }) => {
  109. const blockDirectives = directives
  110. .filter(directive => directive.type === "disable" || directive.type === "enable")
  111. .map(directive => Object.assign({}, directive, { unprocessedDirective: directive }))
  112. .sort(compareLocations);
  113. const lineDirectives = lodash.flatMap(directives, directive => {
  114. switch (directive.type) {
  115. case "disable":
  116. case "enable":
  117. return [];
  118. case "disable-line":
  119. return [
  120. { type: "disable", line: directive.line, column: 1, ruleId: directive.ruleId, unprocessedDirective: directive },
  121. { type: "enable", line: directive.line + 1, column: 0, ruleId: directive.ruleId, unprocessedDirective: directive }
  122. ];
  123. case "disable-next-line":
  124. return [
  125. { type: "disable", line: directive.line + 1, column: 1, ruleId: directive.ruleId, unprocessedDirective: directive },
  126. { type: "enable", line: directive.line + 2, column: 0, ruleId: directive.ruleId, unprocessedDirective: directive }
  127. ];
  128. default:
  129. throw new TypeError(`Unrecognized directive type '${directive.type}'`);
  130. }
  131. }).sort(compareLocations);
  132. const blockDirectivesResult = applyDirectives({
  133. problems,
  134. directives: blockDirectives,
  135. reportUnusedDisableDirectives
  136. });
  137. const lineDirectivesResult = applyDirectives({
  138. problems: blockDirectivesResult.problems,
  139. directives: lineDirectives,
  140. reportUnusedDisableDirectives
  141. });
  142. return reportUnusedDisableDirectives !== "off"
  143. ? lineDirectivesResult.problems
  144. .concat(blockDirectivesResult.unusedDisableDirectives)
  145. .concat(lineDirectivesResult.unusedDisableDirectives)
  146. .sort(compareLocations)
  147. : lineDirectivesResult.problems;
  148. };