• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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