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 | ;
|
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 | */
|
30 |
|
31 | /**
|
32 | * Information about the report
|
33 | * @typedef {Object} ReportInfo
|
34 | * @property {string} ruleId
|
35 | * @property {(0|1|2)} severity
|
36 | * @property {(string|undefined)} message
|
37 | * @property {(string|undefined)} messageId
|
38 | * @property {number} line
|
39 | * @property {number} column
|
40 | * @property {(number|undefined)} endLine
|
41 | * @property {(number|undefined)} endColumn
|
42 | * @property {(string|null)} nodeType
|
43 | * @property {string} source
|
44 | * @property {({text: string, range: (number[]|null)}|null)} fix
|
45 | */
|
46 |
|
47 | //------------------------------------------------------------------------------
|
48 | // Module Definition
|
49 | //------------------------------------------------------------------------------
|
50 |
|
51 |
|
52 | /**
|
53 | * Translates a multi-argument context.report() call into a single object argument call
|
54 | * @param {...*} args A list of arguments passed to `context.report`
|
55 | * @returns {MessageDescriptor} A normalized object containing report information
|
56 | */
|
57 | function normalizeMultiArgReportCall(...args) {
|
58 |
|
59 | // If there is one argument, it is considered to be a new-style call already.
|
60 | if (args.length === 1) {
|
61 |
|
62 | // Shallow clone the object to avoid surprises if reusing the descriptor
|
63 | return Object.assign({}, args[0]);
|
64 | }
|
65 |
|
66 | // If the second argument is a string, the arguments are interpreted as [node, message, data, fix].
|
67 | if (typeof args[1] === "string") {
|
68 | return {
|
69 | node: args[0],
|
70 | message: args[1],
|
71 | data: args[2],
|
72 | fix: args[3]
|
73 | };
|
74 | }
|
75 |
|
76 | // Otherwise, the arguments are interpreted as [node, loc, message, data, fix].
|
77 | return {
|
78 | node: args[0],
|
79 | loc: args[1],
|
80 | message: args[2],
|
81 | data: args[3],
|
82 | fix: args[4]
|
83 | };
|
84 | }
|
85 |
|
86 | /**
|
87 | * Asserts that either a loc or a node was provided, and the node is valid if it was provided.
|
88 | * @param {MessageDescriptor} descriptor A descriptor to validate
|
89 | * @returns {void}
|
90 | * @throws AssertionError if neither a node nor a loc was provided, or if the node is not an object
|
91 | */
|
92 | function assertValidNodeInfo(descriptor) {
|
93 | if (descriptor.node) {
|
94 | assert(typeof descriptor.node === "object", "Node must be an object");
|
95 | } else {
|
96 | assert(descriptor.loc, "Node must be provided when reporting error if location is not provided");
|
97 | }
|
98 | }
|
99 |
|
100 | /**
|
101 | * Normalizes a MessageDescriptor to always have a `loc` with `start` and `end` properties
|
102 | * @param {MessageDescriptor} descriptor A descriptor for the report from a rule.
|
103 | * @returns {{start: Location, end: (Location|null)}} An updated location that infers the `start` and `end` properties
|
104 | * from the `node` of the original descriptor, or infers the `start` from the `loc` of the original descriptor.
|
105 | */
|
106 | function normalizeReportLoc(descriptor) {
|
107 | if (descriptor.loc) {
|
108 | if (descriptor.loc.start) {
|
109 | return descriptor.loc;
|
110 | }
|
111 | return { start: descriptor.loc, end: null };
|
112 | }
|
113 | return descriptor.node.loc;
|
114 | }
|
115 |
|
116 | /**
|
117 | * Compares items in a fixes array by range.
|
118 | * @param {Fix} a The first message.
|
119 | * @param {Fix} b The second message.
|
120 | * @returns {int} -1 if a comes before b, 1 if a comes after b, 0 if equal.
|
121 | * @private
|
122 | */
|
123 | function compareFixesByRange(a, b) {
|
124 | return a.range[0] - b.range[0] || a.range[1] - b.range[1];
|
125 | }
|
126 |
|
127 | /**
|
128 | * Merges the given fixes array into one.
|
129 | * @param {Fix[]} fixes The fixes to merge.
|
130 | * @param {SourceCode} sourceCode The source code object to get the text between fixes.
|
131 | * @returns {{text: string, range: number[]}} The merged fixes
|
132 | */
|
133 | function mergeFixes(fixes, sourceCode) {
|
134 | if (fixes.length === 0) {
|
135 | return null;
|
136 | }
|
137 | if (fixes.length === 1) {
|
138 | return fixes[0];
|
139 | }
|
140 |
|
141 | fixes.sort(compareFixesByRange);
|
142 |
|
143 | const originalText = sourceCode.text;
|
144 | const start = fixes[0].range[0];
|
145 | const end = fixes[fixes.length - 1].range[1];
|
146 | let text = "";
|
147 | let lastPos = Number.MIN_SAFE_INTEGER;
|
148 |
|
149 | for (const fix of fixes) {
|
150 | assert(fix.range[0] >= lastPos, "Fix objects must not be overlapped in a report.");
|
151 |
|
152 | if (fix.range[0] >= 0) {
|
153 | text += originalText.slice(Math.max(0, start, lastPos), fix.range[0]);
|
154 | }
|
155 | text += fix.text;
|
156 | lastPos = fix.range[1];
|
157 | }
|
158 | text += originalText.slice(Math.max(0, start, lastPos), end);
|
159 |
|
160 | return { range: [start, end], text };
|
161 | }
|
162 |
|
163 | /**
|
164 | * Gets one fix object from the given descriptor.
|
165 | * If the descriptor retrieves multiple fixes, this merges those to one.
|
166 | * @param {MessageDescriptor} descriptor The report descriptor.
|
167 | * @param {SourceCode} sourceCode The source code object to get text between fixes.
|
168 | * @returns {({text: string, range: number[]}|null)} The fix for the descriptor
|
169 | */
|
170 | function normalizeFixes(descriptor, sourceCode) {
|
171 | if (typeof descriptor.fix !== "function") {
|
172 | return null;
|
173 | }
|
174 |
|
175 | // @type {null | Fix | Fix[] | IterableIterator<Fix>}
|
176 | const fix = descriptor.fix(ruleFixer);
|
177 |
|
178 | // Merge to one.
|
179 | if (fix && Symbol.iterator in fix) {
|
180 | return mergeFixes(Array.from(fix), sourceCode);
|
181 | }
|
182 | return fix;
|
183 | }
|
184 |
|
185 | /**
|
186 | * Creates information about the report from a descriptor
|
187 | * @param {Object} options Information about the problem
|
188 | * @param {string} options.ruleId Rule ID
|
189 | * @param {(0|1|2)} options.severity Rule severity
|
190 | * @param {(ASTNode|null)} options.node Node
|
191 | * @param {string} options.message Error message
|
192 | * @param {string} [options.messageId] The error message ID.
|
193 | * @param {{start: SourceLocation, end: (SourceLocation|null)}} options.loc Start and end location
|
194 | * @param {{text: string, range: (number[]|null)}} options.fix The fix object
|
195 | * @returns {function(...args): ReportInfo} Function that returns information about the report
|
196 | */
|
197 | function createProblem(options) {
|
198 | const problem = {
|
199 | ruleId: options.ruleId,
|
200 | severity: options.severity,
|
201 | message: options.message,
|
202 | line: options.loc.start.line,
|
203 | column: options.loc.start.column + 1,
|
204 | nodeType: options.node && options.node.type || null
|
205 | };
|
206 |
|
207 | /*
|
208 | * If this isn’t in the conditional, some of the tests fail
|
209 | * because `messageId` is present in the problem object
|
210 | */
|
211 | if (options.messageId) {
|
212 | problem.messageId = options.messageId;
|
213 | }
|
214 |
|
215 | if (options.loc.end) {
|
216 | problem.endLine = options.loc.end.line;
|
217 | problem.endColumn = options.loc.end.column + 1;
|
218 | }
|
219 |
|
220 | if (options.fix) {
|
221 | problem.fix = options.fix;
|
222 | }
|
223 |
|
224 | return problem;
|
225 | }
|
226 |
|
227 | /**
|
228 | * Returns a function that converts the arguments of a `context.report` call from a rule into a reported
|
229 | * problem for the Node.js API.
|
230 | * @param {{ruleId: string, severity: number, sourceCode: SourceCode, messageIds: Object}} metadata Metadata for the reported problem
|
231 | * @param {SourceCode} sourceCode The `SourceCode` instance for the text being linted
|
232 | * @returns {function(...args): ReportInfo} Function that returns information about the report
|
233 | */
|
234 |
|
235 | module.exports = function createReportTranslator(metadata) {
|
236 |
|
237 | /*
|
238 | * `createReportTranslator` gets called once per enabled rule per file. It needs to be very performant.
|
239 | * The report translator itself (i.e. the function that `createReportTranslator` returns) gets
|
240 | * called every time a rule reports a problem, which happens much less frequently (usually, the vast
|
241 | * majority of rules don't report any problems for a given file).
|
242 | */
|
243 | return (...args) => {
|
244 | const descriptor = normalizeMultiArgReportCall(...args);
|
245 |
|
246 | assertValidNodeInfo(descriptor);
|
247 |
|
248 | let computedMessage;
|
249 |
|
250 | if (descriptor.messageId) {
|
251 | if (!metadata.messageIds) {
|
252 | throw new TypeError("context.report() called with a messageId, but no messages were present in the rule metadata.");
|
253 | }
|
254 | const id = descriptor.messageId;
|
255 | const messages = metadata.messageIds;
|
256 |
|
257 | if (descriptor.message) {
|
258 | throw new TypeError("context.report() called with a message and a messageId. Please only pass one.");
|
259 | }
|
260 | if (!messages || !Object.prototype.hasOwnProperty.call(messages, id)) {
|
261 | throw new TypeError(`context.report() called with a messageId of '${id}' which is not present in the 'messages' config: ${JSON.stringify(messages, null, 2)}`);
|
262 | }
|
263 | computedMessage = messages[id];
|
264 | } else if (descriptor.message) {
|
265 | computedMessage = descriptor.message;
|
266 | } else {
|
267 | throw new TypeError("Missing `message` property in report() call; add a message that describes the linting problem.");
|
268 | }
|
269 |
|
270 |
|
271 | return createProblem({
|
272 | ruleId: metadata.ruleId,
|
273 | severity: metadata.severity,
|
274 | node: descriptor.node,
|
275 | message: interpolate(computedMessage, descriptor.data),
|
276 | messageId: descriptor.messageId,
|
277 | loc: normalizeReportLoc(descriptor),
|
278 | fix: normalizeFixes(descriptor, metadata.sourceCode)
|
279 | });
|
280 | };
|
281 | };
|