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