.gitignore added
[dotfiles/.git] / .config / coc / extensions / node_modules / coc-prettier / node_modules / eslint / lib / linter / report-translator.js
1 /**
2  * @fileoverview A helper that translates context.report() calls from the rule API into generic problem objects
3  * @author Teddy Katz
4  */
5
6 "use strict";
7
8 //------------------------------------------------------------------------------
9 // Requirements
10 //------------------------------------------------------------------------------
11
12 const assert = require("assert");
13 const ruleFixer = require("./rule-fixer");
14 const interpolate = require("./interpolate");
15
16 //------------------------------------------------------------------------------
17 // Typedefs
18 //------------------------------------------------------------------------------
19
20 /**
21  * An error message description
22  * @typedef {Object} MessageDescriptor
23  * @property {ASTNode} [node] The reported node
24  * @property {Location} loc The location of the problem.
25  * @property {string} message The problem message.
26  * @property {Object} [data] Optional data to use to fill in placeholders in the
27  *      message.
28  * @property {Function} [fix] The function to call that creates a fix command.
29  * @property {Array<{desc?: string, messageId?: string, fix: Function}>} suggest Suggestion descriptions and functions to create a the associated fixes.
30  */
31
32 /**
33  * Information about the report
34  * @typedef {Object} ReportInfo
35  * @property {string} ruleId
36  * @property {(0|1|2)} severity
37  * @property {(string|undefined)} message
38  * @property {(string|undefined)} [messageId]
39  * @property {number} line
40  * @property {number} column
41  * @property {(number|undefined)} [endLine]
42  * @property {(number|undefined)} [endColumn]
43  * @property {(string|null)} nodeType
44  * @property {string} source
45  * @property {({text: string, range: (number[]|null)}|null)} [fix]
46  * @property {Array<{text: string, range: (number[]|null)}|null>} [suggestions]
47  */
48
49 //------------------------------------------------------------------------------
50 // Module Definition
51 //------------------------------------------------------------------------------
52
53
54 /**
55  * Translates a multi-argument context.report() call into a single object argument call
56  * @param {...*} args A list of arguments passed to `context.report`
57  * @returns {MessageDescriptor} A normalized object containing report information
58  */
59 function normalizeMultiArgReportCall(...args) {
60
61     // If there is one argument, it is considered to be a new-style call already.
62     if (args.length === 1) {
63
64         // Shallow clone the object to avoid surprises if reusing the descriptor
65         return Object.assign({}, args[0]);
66     }
67
68     // If the second argument is a string, the arguments are interpreted as [node, message, data, fix].
69     if (typeof args[1] === "string") {
70         return {
71             node: args[0],
72             message: args[1],
73             data: args[2],
74             fix: args[3]
75         };
76     }
77
78     // Otherwise, the arguments are interpreted as [node, loc, message, data, fix].
79     return {
80         node: args[0],
81         loc: args[1],
82         message: args[2],
83         data: args[3],
84         fix: args[4]
85     };
86 }
87
88 /**
89  * Asserts that either a loc or a node was provided, and the node is valid if it was provided.
90  * @param {MessageDescriptor} descriptor A descriptor to validate
91  * @returns {void}
92  * @throws AssertionError if neither a node nor a loc was provided, or if the node is not an object
93  */
94 function assertValidNodeInfo(descriptor) {
95     if (descriptor.node) {
96         assert(typeof descriptor.node === "object", "Node must be an object");
97     } else {
98         assert(descriptor.loc, "Node must be provided when reporting error if location is not provided");
99     }
100 }
101
102 /**
103  * Normalizes a MessageDescriptor to always have a `loc` with `start` and `end` properties
104  * @param {MessageDescriptor} descriptor A descriptor for the report from a rule.
105  * @returns {{start: Location, end: (Location|null)}} An updated location that infers the `start` and `end` properties
106  * from the `node` of the original descriptor, or infers the `start` from the `loc` of the original descriptor.
107  */
108 function normalizeReportLoc(descriptor) {
109     if (descriptor.loc) {
110         if (descriptor.loc.start) {
111             return descriptor.loc;
112         }
113         return { start: descriptor.loc, end: null };
114     }
115     return descriptor.node.loc;
116 }
117
118 /**
119  * Check that a fix has a valid range.
120  * @param {Fix|null} fix The fix to validate.
121  * @returns {void}
122  */
123 function assertValidFix(fix) {
124     if (fix) {
125         assert(fix.range && typeof fix.range[0] === "number" && typeof fix.range[1] === "number", `Fix has invalid range: ${JSON.stringify(fix, null, 2)}`);
126     }
127 }
128
129 /**
130  * Compares items in a fixes array by range.
131  * @param {Fix} a The first message.
132  * @param {Fix} b The second message.
133  * @returns {int} -1 if a comes before b, 1 if a comes after b, 0 if equal.
134  * @private
135  */
136 function compareFixesByRange(a, b) {
137     return a.range[0] - b.range[0] || a.range[1] - b.range[1];
138 }
139
140 /**
141  * Merges the given fixes array into one.
142  * @param {Fix[]} fixes The fixes to merge.
143  * @param {SourceCode} sourceCode The source code object to get the text between fixes.
144  * @returns {{text: string, range: number[]}} The merged fixes
145  */
146 function mergeFixes(fixes, sourceCode) {
147     for (const fix of fixes) {
148         assertValidFix(fix);
149     }
150
151     if (fixes.length === 0) {
152         return null;
153     }
154     if (fixes.length === 1) {
155         return fixes[0];
156     }
157
158     fixes.sort(compareFixesByRange);
159
160     const originalText = sourceCode.text;
161     const start = fixes[0].range[0];
162     const end = fixes[fixes.length - 1].range[1];
163     let text = "";
164     let lastPos = Number.MIN_SAFE_INTEGER;
165
166     for (const fix of fixes) {
167         assert(fix.range[0] >= lastPos, "Fix objects must not be overlapped in a report.");
168
169         if (fix.range[0] >= 0) {
170             text += originalText.slice(Math.max(0, start, lastPos), fix.range[0]);
171         }
172         text += fix.text;
173         lastPos = fix.range[1];
174     }
175     text += originalText.slice(Math.max(0, start, lastPos), end);
176
177     return { range: [start, end], text };
178 }
179
180 /**
181  * Gets one fix object from the given descriptor.
182  * If the descriptor retrieves multiple fixes, this merges those to one.
183  * @param {MessageDescriptor} descriptor The report descriptor.
184  * @param {SourceCode} sourceCode The source code object to get text between fixes.
185  * @returns {({text: string, range: number[]}|null)} The fix for the descriptor
186  */
187 function normalizeFixes(descriptor, sourceCode) {
188     if (typeof descriptor.fix !== "function") {
189         return null;
190     }
191
192     // @type {null | Fix | Fix[] | IterableIterator<Fix>}
193     const fix = descriptor.fix(ruleFixer);
194
195     // Merge to one.
196     if (fix && Symbol.iterator in fix) {
197         return mergeFixes(Array.from(fix), sourceCode);
198     }
199
200     assertValidFix(fix);
201     return fix;
202 }
203
204 /**
205  * Gets an array of suggestion objects from the given descriptor.
206  * @param {MessageDescriptor} descriptor The report descriptor.
207  * @param {SourceCode} sourceCode The source code object to get text between fixes.
208  * @param {Object} messages Object of meta messages for the rule.
209  * @returns {Array<SuggestionResult>} The suggestions for the descriptor
210  */
211 function mapSuggestions(descriptor, sourceCode, messages) {
212     if (!descriptor.suggest || !Array.isArray(descriptor.suggest)) {
213         return [];
214     }
215
216     return descriptor.suggest
217         .map(suggestInfo => {
218             const computedDesc = suggestInfo.desc || messages[suggestInfo.messageId];
219
220             return {
221                 ...suggestInfo,
222                 desc: interpolate(computedDesc, suggestInfo.data),
223                 fix: normalizeFixes(suggestInfo, sourceCode)
224             };
225         })
226
227         // Remove suggestions that didn't provide a fix
228         .filter(({ fix }) => fix);
229 }
230
231 /**
232  * Creates information about the report from a descriptor
233  * @param {Object} options Information about the problem
234  * @param {string} options.ruleId Rule ID
235  * @param {(0|1|2)} options.severity Rule severity
236  * @param {(ASTNode|null)} options.node Node
237  * @param {string} options.message Error message
238  * @param {string} [options.messageId] The error message ID.
239  * @param {{start: SourceLocation, end: (SourceLocation|null)}} options.loc Start and end location
240  * @param {{text: string, range: (number[]|null)}} options.fix The fix object
241  * @param {Array<{text: string, range: (number[]|null)}>} options.suggestions The array of suggestions objects
242  * @returns {function(...args): ReportInfo} Function that returns information about the report
243  */
244 function createProblem(options) {
245     const problem = {
246         ruleId: options.ruleId,
247         severity: options.severity,
248         message: options.message,
249         line: options.loc.start.line,
250         column: options.loc.start.column + 1,
251         nodeType: options.node && options.node.type || null
252     };
253
254     /*
255      * If this isn’t in the conditional, some of the tests fail
256      * because `messageId` is present in the problem object
257      */
258     if (options.messageId) {
259         problem.messageId = options.messageId;
260     }
261
262     if (options.loc.end) {
263         problem.endLine = options.loc.end.line;
264         problem.endColumn = options.loc.end.column + 1;
265     }
266
267     if (options.fix) {
268         problem.fix = options.fix;
269     }
270
271     if (options.suggestions && options.suggestions.length > 0) {
272         problem.suggestions = options.suggestions;
273     }
274
275     return problem;
276 }
277
278 /**
279  * Validates that suggestions are properly defined. Throws if an error is detected.
280  * @param {Array<{ desc?: string, messageId?: string }>} suggest The incoming suggest data.
281  * @param {Object} messages Object of meta messages for the rule.
282  * @returns {void}
283  */
284 function validateSuggestions(suggest, messages) {
285     if (suggest && Array.isArray(suggest)) {
286         suggest.forEach(suggestion => {
287             if (suggestion.messageId) {
288                 const { messageId } = suggestion;
289
290                 if (!messages) {
291                     throw new TypeError(`context.report() called with a suggest option with a messageId '${messageId}', but no messages were present in the rule metadata.`);
292                 }
293
294                 if (!messages[messageId]) {
295                     throw new TypeError(`context.report() called with a suggest option with a messageId '${messageId}' which is not present in the 'messages' config: ${JSON.stringify(messages, null, 2)}`);
296                 }
297
298                 if (suggestion.desc) {
299                     throw new TypeError("context.report() called with a suggest option that defines both a 'messageId' and an 'desc'. Please only pass one.");
300                 }
301             } else if (!suggestion.desc) {
302                 throw new TypeError("context.report() called with a suggest option that doesn't have either a `desc` or `messageId`");
303             }
304
305             if (typeof suggestion.fix !== "function") {
306                 throw new TypeError(`context.report() called with a suggest option without a fix function. See: ${suggestion}`);
307             }
308         });
309     }
310 }
311
312 /**
313  * Returns a function that converts the arguments of a `context.report` call from a rule into a reported
314  * problem for the Node.js API.
315  * @param {{ruleId: string, severity: number, sourceCode: SourceCode, messageIds: Object, disableFixes: boolean}} metadata Metadata for the reported problem
316  * @param {SourceCode} sourceCode The `SourceCode` instance for the text being linted
317  * @returns {function(...args): ReportInfo} Function that returns information about the report
318  */
319
320 module.exports = function createReportTranslator(metadata) {
321
322     /*
323      * `createReportTranslator` gets called once per enabled rule per file. It needs to be very performant.
324      * The report translator itself (i.e. the function that `createReportTranslator` returns) gets
325      * called every time a rule reports a problem, which happens much less frequently (usually, the vast
326      * majority of rules don't report any problems for a given file).
327      */
328     return (...args) => {
329         const descriptor = normalizeMultiArgReportCall(...args);
330         const messages = metadata.messageIds;
331
332         assertValidNodeInfo(descriptor);
333
334         let computedMessage;
335
336         if (descriptor.messageId) {
337             if (!messages) {
338                 throw new TypeError("context.report() called with a messageId, but no messages were present in the rule metadata.");
339             }
340             const id = descriptor.messageId;
341
342             if (descriptor.message) {
343                 throw new TypeError("context.report() called with a message and a messageId. Please only pass one.");
344             }
345             if (!messages || !Object.prototype.hasOwnProperty.call(messages, id)) {
346                 throw new TypeError(`context.report() called with a messageId of '${id}' which is not present in the 'messages' config: ${JSON.stringify(messages, null, 2)}`);
347             }
348             computedMessage = messages[id];
349         } else if (descriptor.message) {
350             computedMessage = descriptor.message;
351         } else {
352             throw new TypeError("Missing `message` property in report() call; add a message that describes the linting problem.");
353         }
354
355         validateSuggestions(descriptor.suggest, messages);
356
357         return createProblem({
358             ruleId: metadata.ruleId,
359             severity: metadata.severity,
360             node: descriptor.node,
361             message: interpolate(computedMessage, descriptor.data),
362             messageId: descriptor.messageId,
363             loc: normalizeReportLoc(descriptor),
364             fix: metadata.disableFixes ? null : normalizeFixes(descriptor, metadata.sourceCode),
365             suggestions: metadata.disableFixes ? [] : mapSuggestions(descriptor, metadata.sourceCode, messages)
366         });
367     };
368 };