source-code-fixer.js 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  1. /**
  2. * @fileoverview An object that caches and applies source code fixes.
  3. * @author Nicholas C. Zakas
  4. */
  5. "use strict";
  6. //------------------------------------------------------------------------------
  7. // Requirements
  8. //------------------------------------------------------------------------------
  9. const debug = require("debug")("eslint:source-code-fixer");
  10. //------------------------------------------------------------------------------
  11. // Helpers
  12. //------------------------------------------------------------------------------
  13. const BOM = "\uFEFF";
  14. /**
  15. * Compares items in a messages array by range.
  16. * @param {Message} a The first message.
  17. * @param {Message} b The second message.
  18. * @returns {int} -1 if a comes before b, 1 if a comes after b, 0 if equal.
  19. * @private
  20. */
  21. function compareMessagesByFixRange(a, b) {
  22. return a.fix.range[0] - b.fix.range[0] || a.fix.range[1] - b.fix.range[1];
  23. }
  24. /**
  25. * Compares items in a messages array by line and column.
  26. * @param {Message} a The first message.
  27. * @param {Message} b The second message.
  28. * @returns {int} -1 if a comes before b, 1 if a comes after b, 0 if equal.
  29. * @private
  30. */
  31. function compareMessagesByLocation(a, b) {
  32. return a.line - b.line || a.column - b.column;
  33. }
  34. //------------------------------------------------------------------------------
  35. // Public Interface
  36. //------------------------------------------------------------------------------
  37. /**
  38. * Utility for apply fixes to source code.
  39. * @constructor
  40. */
  41. function SourceCodeFixer() {
  42. Object.freeze(this);
  43. }
  44. /**
  45. * Applies the fixes specified by the messages to the given text. Tries to be
  46. * smart about the fixes and won't apply fixes over the same area in the text.
  47. * @param {string} sourceText The text to apply the changes to.
  48. * @param {Message[]} messages The array of messages reported by ESLint.
  49. * @param {boolean|Function} [shouldFix=true] Determines whether each message should be fixed
  50. * @returns {Object} An object containing the fixed text and any unfixed messages.
  51. */
  52. SourceCodeFixer.applyFixes = function(sourceText, messages, shouldFix) {
  53. debug("Applying fixes");
  54. if (shouldFix === false) {
  55. debug("shouldFix parameter was false, not attempting fixes");
  56. return {
  57. fixed: false,
  58. messages,
  59. output: sourceText
  60. };
  61. }
  62. // clone the array
  63. const remainingMessages = [],
  64. fixes = [],
  65. bom = sourceText.startsWith(BOM) ? BOM : "",
  66. text = bom ? sourceText.slice(1) : sourceText;
  67. let lastPos = Number.NEGATIVE_INFINITY,
  68. output = bom;
  69. /**
  70. * Try to use the 'fix' from a problem.
  71. * @param {Message} problem The message object to apply fixes from
  72. * @returns {boolean} Whether fix was successfully applied
  73. */
  74. function attemptFix(problem) {
  75. const fix = problem.fix;
  76. const start = fix.range[0];
  77. const end = fix.range[1];
  78. // Remain it as a problem if it's overlapped or it's a negative range
  79. if (lastPos >= start || start > end) {
  80. remainingMessages.push(problem);
  81. return false;
  82. }
  83. // Remove BOM.
  84. if ((start < 0 && end >= 0) || (start === 0 && fix.text.startsWith(BOM))) {
  85. output = "";
  86. }
  87. // Make output to this fix.
  88. output += text.slice(Math.max(0, lastPos), Math.max(0, start));
  89. output += fix.text;
  90. lastPos = end;
  91. return true;
  92. }
  93. messages.forEach(problem => {
  94. if (Object.prototype.hasOwnProperty.call(problem, "fix")) {
  95. fixes.push(problem);
  96. } else {
  97. remainingMessages.push(problem);
  98. }
  99. });
  100. if (fixes.length) {
  101. debug("Found fixes to apply");
  102. let fixesWereApplied = false;
  103. for (const problem of fixes.sort(compareMessagesByFixRange)) {
  104. if (typeof shouldFix !== "function" || shouldFix(problem)) {
  105. attemptFix(problem);
  106. /*
  107. * The only time attemptFix will fail is if a previous fix was
  108. * applied which conflicts with it. So we can mark this as true.
  109. */
  110. fixesWereApplied = true;
  111. } else {
  112. remainingMessages.push(problem);
  113. }
  114. }
  115. output += text.slice(Math.max(0, lastPos));
  116. return {
  117. fixed: fixesWereApplied,
  118. messages: remainingMessages.sort(compareMessagesByLocation),
  119. output
  120. };
  121. }
  122. debug("No fixes to apply");
  123. return {
  124. fixed: false,
  125. messages,
  126. output: bom + text
  127. };
  128. };
  129. module.exports = SourceCodeFixer;