| /** |
| * @fileoverview A helper that translates context.report() calls from the rule API into generic problem objects |
| * @author Teddy Katz |
| */ |
| |
| "use strict"; |
| |
| //------------------------------------------------------------------------------ |
| // Requirements |
| //------------------------------------------------------------------------------ |
| |
| const assert = require("assert"); |
| const ruleFixer = require("./rule-fixer"); |
| const interpolate = require("./interpolate"); |
| |
| //------------------------------------------------------------------------------ |
| // Typedefs |
| //------------------------------------------------------------------------------ |
| |
| /** |
| * An error message description |
| * @typedef {Object} MessageDescriptor |
| * @property {ASTNode} [node] The reported node |
| * @property {Location} loc The location of the problem. |
| * @property {string} message The problem message. |
| * @property {Object} [data] Optional data to use to fill in placeholders in the |
| * message. |
| * @property {Function} [fix] The function to call that creates a fix command. |
| */ |
| |
| /** |
| * Information about the report |
| * @typedef {Object} ReportInfo |
| * @property {string} ruleId |
| * @property {(0|1|2)} severity |
| * @property {(string|undefined)} message |
| * @property {(string|undefined)} messageId |
| * @property {number} line |
| * @property {number} column |
| * @property {(number|undefined)} endLine |
| * @property {(number|undefined)} endColumn |
| * @property {(string|null)} nodeType |
| * @property {string} source |
| * @property {({text: string, range: (number[]|null)}|null)} fix |
| */ |
| |
| //------------------------------------------------------------------------------ |
| // Module Definition |
| //------------------------------------------------------------------------------ |
| |
| |
| /** |
| * Translates a multi-argument context.report() call into a single object argument call |
| * @param {...*} args A list of arguments passed to `context.report` |
| * @returns {MessageDescriptor} A normalized object containing report information |
| */ |
| function normalizeMultiArgReportCall(...args) { |
| |
| // If there is one argument, it is considered to be a new-style call already. |
| if (args.length === 1) { |
| |
| // Shallow clone the object to avoid surprises if reusing the descriptor |
| return Object.assign({}, args[0]); |
| } |
| |
| // If the second argument is a string, the arguments are interpreted as [node, message, data, fix]. |
| if (typeof args[1] === "string") { |
| return { |
| node: args[0], |
| message: args[1], |
| data: args[2], |
| fix: args[3] |
| }; |
| } |
| |
| // Otherwise, the arguments are interpreted as [node, loc, message, data, fix]. |
| return { |
| node: args[0], |
| loc: args[1], |
| message: args[2], |
| data: args[3], |
| fix: args[4] |
| }; |
| } |
| |
| /** |
| * Asserts that either a loc or a node was provided, and the node is valid if it was provided. |
| * @param {MessageDescriptor} descriptor A descriptor to validate |
| * @returns {void} |
| * @throws AssertionError if neither a node nor a loc was provided, or if the node is not an object |
| */ |
| function assertValidNodeInfo(descriptor) { |
| if (descriptor.node) { |
| assert(typeof descriptor.node === "object", "Node must be an object"); |
| } else { |
| assert(descriptor.loc, "Node must be provided when reporting error if location is not provided"); |
| } |
| } |
| |
| /** |
| * Normalizes a MessageDescriptor to always have a `loc` with `start` and `end` properties |
| * @param {MessageDescriptor} descriptor A descriptor for the report from a rule. |
| * @returns {{start: Location, end: (Location|null)}} An updated location that infers the `start` and `end` properties |
| * from the `node` of the original descriptor, or infers the `start` from the `loc` of the original descriptor. |
| */ |
| function normalizeReportLoc(descriptor) { |
| if (descriptor.loc) { |
| if (descriptor.loc.start) { |
| return descriptor.loc; |
| } |
| return { start: descriptor.loc, end: null }; |
| } |
| return descriptor.node.loc; |
| } |
| |
| /** |
| * Compares items in a fixes array by range. |
| * @param {Fix} a The first message. |
| * @param {Fix} b The second message. |
| * @returns {int} -1 if a comes before b, 1 if a comes after b, 0 if equal. |
| * @private |
| */ |
| function compareFixesByRange(a, b) { |
| return a.range[0] - b.range[0] || a.range[1] - b.range[1]; |
| } |
| |
| /** |
| * Merges the given fixes array into one. |
| * @param {Fix[]} fixes The fixes to merge. |
| * @param {SourceCode} sourceCode The source code object to get the text between fixes. |
| * @returns {{text: string, range: number[]}} The merged fixes |
| */ |
| function mergeFixes(fixes, sourceCode) { |
| if (fixes.length === 0) { |
| return null; |
| } |
| if (fixes.length === 1) { |
| return fixes[0]; |
| } |
| |
| fixes.sort(compareFixesByRange); |
| |
| const originalText = sourceCode.text; |
| const start = fixes[0].range[0]; |
| const end = fixes[fixes.length - 1].range[1]; |
| let text = ""; |
| let lastPos = Number.MIN_SAFE_INTEGER; |
| |
| for (const fix of fixes) { |
| assert(fix.range[0] >= lastPos, "Fix objects must not be overlapped in a report."); |
| |
| if (fix.range[0] >= 0) { |
| text += originalText.slice(Math.max(0, start, lastPos), fix.range[0]); |
| } |
| text += fix.text; |
| lastPos = fix.range[1]; |
| } |
| text += originalText.slice(Math.max(0, start, lastPos), end); |
| |
| return { range: [start, end], text }; |
| } |
| |
| /** |
| * Gets one fix object from the given descriptor. |
| * If the descriptor retrieves multiple fixes, this merges those to one. |
| * @param {MessageDescriptor} descriptor The report descriptor. |
| * @param {SourceCode} sourceCode The source code object to get text between fixes. |
| * @returns {({text: string, range: number[]}|null)} The fix for the descriptor |
| */ |
| function normalizeFixes(descriptor, sourceCode) { |
| if (typeof descriptor.fix !== "function") { |
| return null; |
| } |
| |
| // @type {null | Fix | Fix[] | IterableIterator<Fix>} |
| const fix = descriptor.fix(ruleFixer); |
| |
| // Merge to one. |
| if (fix && Symbol.iterator in fix) { |
| return mergeFixes(Array.from(fix), sourceCode); |
| } |
| return fix; |
| } |
| |
| /** |
| * Creates information about the report from a descriptor |
| * @param {Object} options Information about the problem |
| * @param {string} options.ruleId Rule ID |
| * @param {(0|1|2)} options.severity Rule severity |
| * @param {(ASTNode|null)} options.node Node |
| * @param {string} options.message Error message |
| * @param {string} [options.messageId] The error message ID. |
| * @param {{start: SourceLocation, end: (SourceLocation|null)}} options.loc Start and end location |
| * @param {{text: string, range: (number[]|null)}} options.fix The fix object |
| * @returns {function(...args): ReportInfo} Function that returns information about the report |
| */ |
| function createProblem(options) { |
| const problem = { |
| ruleId: options.ruleId, |
| severity: options.severity, |
| message: options.message, |
| line: options.loc.start.line, |
| column: options.loc.start.column + 1, |
| nodeType: options.node && options.node.type || null |
| }; |
| |
| /* |
| * If this isn’t in the conditional, some of the tests fail |
| * because `messageId` is present in the problem object |
| */ |
| if (options.messageId) { |
| problem.messageId = options.messageId; |
| } |
| |
| if (options.loc.end) { |
| problem.endLine = options.loc.end.line; |
| problem.endColumn = options.loc.end.column + 1; |
| } |
| |
| if (options.fix) { |
| problem.fix = options.fix; |
| } |
| |
| return problem; |
| } |
| |
| /** |
| * Returns a function that converts the arguments of a `context.report` call from a rule into a reported |
| * problem for the Node.js API. |
| * @param {{ruleId: string, severity: number, sourceCode: SourceCode, messageIds: Object, disableFixes: boolean}} metadata Metadata for the reported problem |
| * @param {SourceCode} sourceCode The `SourceCode` instance for the text being linted |
| * @returns {function(...args): ReportInfo} Function that returns information about the report |
| */ |
| |
| module.exports = function createReportTranslator(metadata) { |
| |
| /* |
| * `createReportTranslator` gets called once per enabled rule per file. It needs to be very performant. |
| * The report translator itself (i.e. the function that `createReportTranslator` returns) gets |
| * called every time a rule reports a problem, which happens much less frequently (usually, the vast |
| * majority of rules don't report any problems for a given file). |
| */ |
| return (...args) => { |
| const descriptor = normalizeMultiArgReportCall(...args); |
| |
| assertValidNodeInfo(descriptor); |
| |
| let computedMessage; |
| |
| if (descriptor.messageId) { |
| if (!metadata.messageIds) { |
| throw new TypeError("context.report() called with a messageId, but no messages were present in the rule metadata."); |
| } |
| const id = descriptor.messageId; |
| const messages = metadata.messageIds; |
| |
| if (descriptor.message) { |
| throw new TypeError("context.report() called with a message and a messageId. Please only pass one."); |
| } |
| if (!messages || !Object.prototype.hasOwnProperty.call(messages, id)) { |
| throw new TypeError(`context.report() called with a messageId of '${id}' which is not present in the 'messages' config: ${JSON.stringify(messages, null, 2)}`); |
| } |
| computedMessage = messages[id]; |
| } else if (descriptor.message) { |
| computedMessage = descriptor.message; |
| } else { |
| throw new TypeError("Missing `message` property in report() call; add a message that describes the linting problem."); |
| } |
| |
| |
| return createProblem({ |
| ruleId: metadata.ruleId, |
| severity: metadata.severity, |
| node: descriptor.node, |
| message: interpolate(computedMessage, descriptor.data), |
| messageId: descriptor.messageId, |
| loc: normalizeReportLoc(descriptor), |
| fix: metadata.disableFixes ? null : normalizeFixes(descriptor, metadata.sourceCode) |
| }); |
| }; |
| }; |