Skip to content

Commit bbf23fa

Browse files
nzakasmdjermanovic
andauthored
fix: Refactor reporting into FileReport (#19877)
* refactor: Reporting into FileReport refs #18787 * Remove unused file * Update tests * Update lib/linter/linter.js Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com> * Refactor runRules * Don't pass language * Update lib/linter/linter.js Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com> * Update lib/linter/linter.js Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com> * Update lib/linter/file-report.js Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com> * Update lib/linter/file-report.js Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com> * Update lib/linter/file-report.js Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com> * Update lib/linter/linter.js Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com> * Update tests/lib/linter/file-report.js Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com> * Update tests/lib/linter/file-report.js Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com> * Add back add* method tests * Fix tests * Update lib/linter/file-report.js Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com> * Update lib/linter/file-report.js Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com> * Update lib/linter/file-report.js Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com> * Update tests/lib/linter/file-report.js Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com> * Update tests/lib/linter/file-report.js Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com> * Update tests/lib/linter/file-report.js Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com> * Update tests/lib/linter/file-report.js Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com> * Update tests/lib/linter/file-report.js Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com> * Update tests/lib/linter/file-report.js Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com> * Update tests/lib/linter/file-report.js Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com> * Clean up tests * Update tests/lib/linter/file-report.js Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com> * Remove methods, return message from others * Fix file comments --------- Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com>
1 parent 74f01a3 commit bbf23fa

File tree

4 files changed

+1277
-758
lines changed

4 files changed

+1277
-758
lines changed
Lines changed: 255 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/**
2-
* @fileoverview A helper that translates context.report() calls from the rule API into generic problem objects
3-
* @author Teddy Katz
2+
* @fileoverview A class to track messages reported by the linter for a file.
3+
* @author Nicholas C. Zakas
44
*/
55

66
"use strict";
@@ -12,13 +12,16 @@
1212
const assert = require("../shared/assert");
1313
const { RuleFixer } = require("./rule-fixer");
1414
const { interpolate } = require("./interpolate");
15+
const ruleReplacements = require("../../conf/replacements.json");
1516

1617
//------------------------------------------------------------------------------
1718
// Typedefs
1819
//------------------------------------------------------------------------------
1920

2021
/** @typedef {import("../types").Linter.LintMessage} LintMessage */
2122
/** @typedef {import("../types").Linter.LintSuggestion} SuggestionResult */
23+
/** @typedef {import("@eslint/core").Language} Language */
24+
/** @typedef {import("@eslint/core").SourceLocation} SourceLocation */
2225

2326
/**
2427
* An error message description
@@ -32,10 +35,99 @@ const { interpolate } = require("./interpolate");
3235
* @property {Array<{desc?: string, messageId?: string, fix: Function}>} suggest Suggestion descriptions and functions to create a the associated fixes.
3336
*/
3437

38+
/**
39+
* @typedef {Object} LintProblem
40+
* @property {string} ruleId The rule ID that reported the problem.
41+
* @property {string} message The problem message.
42+
* @property {SourceLocation} loc The location of the problem.
43+
*/
44+
3545
//------------------------------------------------------------------------------
36-
// Module Definition
46+
// Helpers
3747
//------------------------------------------------------------------------------
3848

49+
const DEFAULT_ERROR_LOC = {
50+
start: { line: 1, column: 0 },
51+
end: { line: 1, column: 1 },
52+
};
53+
54+
/**
55+
* Updates a given location based on the language offsets. This allows us to
56+
* change 0-based locations to 1-based locations. We always want ESLint
57+
* reporting lines and columns starting from 1.
58+
* @todo Potentially this should be moved into a shared utility file.
59+
* @param {Object} location The location to update.
60+
* @param {number} location.line The starting line number.
61+
* @param {number} location.column The starting column number.
62+
* @param {number} [location.endLine] The ending line number.
63+
* @param {number} [location.endColumn] The ending column number.
64+
* @param {Language} language The language to use to adjust the location information.
65+
* @returns {Object} The updated location.
66+
*/
67+
function updateLocationInformation(
68+
{ line, column, endLine, endColumn },
69+
language,
70+
) {
71+
const columnOffset = language.columnStart === 1 ? 0 : 1;
72+
const lineOffset = language.lineStart === 1 ? 0 : 1;
73+
74+
// calculate separately to account for undefined
75+
const finalEndLine = endLine === void 0 ? endLine : endLine + lineOffset;
76+
const finalEndColumn =
77+
endColumn === void 0 ? endColumn : endColumn + columnOffset;
78+
79+
return {
80+
line: line + lineOffset,
81+
column: column + columnOffset,
82+
endLine: finalEndLine,
83+
endColumn: finalEndColumn,
84+
};
85+
}
86+
87+
/**
88+
* creates a missing-rule message.
89+
* @param {string} ruleId the ruleId to create
90+
* @returns {string} created error message
91+
* @private
92+
*/
93+
function createMissingRuleMessage(ruleId) {
94+
return Object.hasOwn(ruleReplacements.rules, ruleId)
95+
? `Rule '${ruleId}' was removed and replaced by: ${ruleReplacements.rules[ruleId].join(", ")}`
96+
: `Definition for rule '${ruleId}' was not found.`;
97+
}
98+
99+
/**
100+
* creates a linting problem
101+
* @param {LintProblem} options to create linting error
102+
* @param {RuleSeverity} severity the error message to report
103+
* @param {Language} language the language to use to adjust the location information.
104+
* @returns {LintMessage} created problem, returns a missing-rule problem if only provided ruleId.
105+
* @private
106+
*/
107+
function createLintingProblem(options, severity, language) {
108+
const {
109+
ruleId = null,
110+
loc = DEFAULT_ERROR_LOC,
111+
message = createMissingRuleMessage(options.ruleId),
112+
} = options;
113+
114+
return {
115+
ruleId,
116+
message,
117+
...updateLocationInformation(
118+
{
119+
line: loc.start.line,
120+
column: loc.start.column,
121+
endLine: loc.end.line,
122+
endColumn: loc.end.column,
123+
},
124+
language,
125+
),
126+
severity,
127+
nodeType: null,
128+
};
129+
}
130+
39131
/**
40132
* Translates a multi-argument context.report() call into a single object argument call
41133
* @param {...*} args A list of arguments passed to `context.report`
@@ -342,73 +434,175 @@ function validateSuggestions(suggest, messages) {
342434
}
343435

344436
/**
345-
* Returns a function that converts the arguments of a `context.report` call from a rule into a reported
346-
* problem for the Node.js API.
347-
* @param {{ruleId: string, severity: number, sourceCode: SourceCode, messageIds: Object, disableFixes: boolean, language:Language}} metadata Metadata for the reported problem
348-
* @returns {function(...args): LintMessage} Function that returns information about the report
437+
* Computes the message from a report descriptor.
438+
* @param {MessageDescriptor} descriptor The report descriptor.
439+
* @param {Object} messages Object of meta messages for the rule.
440+
* @returns {string} The computed message.
441+
* @throws {TypeError} If messageId is not found or both message and messageId are provided.
349442
*/
443+
function computeMessageFromDescriptor(descriptor, messages) {
444+
if (descriptor.messageId) {
445+
if (!messages) {
446+
throw new TypeError(
447+
"context.report() called with a messageId, but no messages were present in the rule metadata.",
448+
);
449+
}
450+
const id = descriptor.messageId;
350451

351-
module.exports = function createReportTranslator(metadata) {
352-
/*
353-
* `createReportTranslator` gets called once per enabled rule per file. It needs to be very performant.
354-
* The report translator itself (i.e. the function that `createReportTranslator` returns) gets
355-
* called every time a rule reports a problem, which happens much less frequently (usually, the vast
356-
* majority of rules don't report any problems for a given file).
452+
if (descriptor.message) {
453+
throw new TypeError(
454+
"context.report() called with a message and a messageId. Please only pass one.",
455+
);
456+
}
457+
if (!messages || !Object.hasOwn(messages, id)) {
458+
throw new TypeError(
459+
`context.report() called with a messageId of '${id}' which is not present in the 'messages' config: ${JSON.stringify(messages, null, 2)}`,
460+
);
461+
}
462+
return messages[id];
463+
}
464+
465+
if (descriptor.message) {
466+
return descriptor.message;
467+
}
468+
469+
throw new TypeError(
470+
"Missing `message` property in report() call; add a message that describes the linting problem.",
471+
);
472+
}
473+
474+
/**
475+
* A report object that contains the messages reported the linter
476+
* for a file.
477+
*/
478+
class FileReport {
479+
/**
480+
* The messages reported by the linter for this file.
481+
* @type {LintMessage[]}
482+
*/
483+
messages = [];
484+
485+
/**
486+
* A rule mapper that maps rule IDs to their metadata.
487+
* @type {(string) => RuleDefinition}
488+
*/
489+
#ruleMapper;
490+
491+
/**
492+
* The source code object for the file.
493+
* @type {SourceCode}
494+
*/
495+
#sourceCode;
496+
497+
/**
498+
* The language to use to adjust line and column offsets.
499+
* @type {Language}
500+
*/
501+
#language;
502+
503+
/**
504+
* Whether to disable fixes for this report.
505+
* @type {boolean}
357506
*/
358-
return (...args) => {
507+
#disableFixes;
508+
509+
/**
510+
* Creates a new FileReport instance.
511+
* @param {Object} options The options for the file report
512+
* @param {(string) => RuleDefinition} options.ruleMapper A rule mapper that maps rule IDs to their metadata.
513+
* @param {SourceCode} options.sourceCode The source code object for the file.
514+
* @param {Language} options.language The language to use to adjust line and column offsets.
515+
* @param {boolean} [options.disableFixes=false] Whether to disable fixes for this report.
516+
*/
517+
constructor({ ruleMapper, sourceCode, language, disableFixes = false }) {
518+
this.#ruleMapper = ruleMapper;
519+
this.#sourceCode = sourceCode;
520+
this.#language = language;
521+
this.#disableFixes = disableFixes;
522+
}
523+
524+
/**
525+
* Adds a rule-generated message to the report.
526+
* @param {string} ruleId The rule ID that reported the problem.
527+
* @param {0|1|2} severity The severity of the problem (0 = off, 1 = warning, 2 = error).
528+
* @param {...*} args The arguments passed to `context.report()`.
529+
* @returns {LintMessage} The created message object.
530+
* @throws {TypeError} If the messageId is not found or both message and messageId are provided.
531+
* @throws {AssertionError} If the node is not an object or neither a node nor a loc is provided.
532+
*/
533+
addRuleMessage(ruleId, severity, ...args) {
359534
const descriptor = normalizeMultiArgReportCall(...args);
360-
const messages = metadata.messageIds;
361-
const { sourceCode } = metadata;
535+
const ruleDefinition = this.#ruleMapper(ruleId);
536+
const messages = ruleDefinition?.meta?.messages;
362537

363538
assertValidNodeInfo(descriptor);
364539

365-
let computedMessage;
540+
const computedMessage = computeMessageFromDescriptor(
541+
descriptor,
542+
messages,
543+
);
366544

367-
if (descriptor.messageId) {
368-
if (!messages) {
369-
throw new TypeError(
370-
"context.report() called with a messageId, but no messages were present in the rule metadata.",
371-
);
372-
}
373-
const id = descriptor.messageId;
545+
validateSuggestions(descriptor.suggest, messages);
374546

375-
if (descriptor.message) {
376-
throw new TypeError(
377-
"context.report() called with a message and a messageId. Please only pass one.",
378-
);
379-
}
380-
if (!messages || !Object.hasOwn(messages, id)) {
381-
throw new TypeError(
382-
`context.report() called with a messageId of '${id}' which is not present in the 'messages' config: ${JSON.stringify(messages, null, 2)}`,
383-
);
384-
}
385-
computedMessage = messages[id];
386-
} else if (descriptor.message) {
387-
computedMessage = descriptor.message;
388-
} else {
389-
throw new TypeError(
390-
"Missing `message` property in report() call; add a message that describes the linting problem.",
391-
);
392-
}
547+
this.messages.push(
548+
createProblem({
549+
ruleId,
550+
severity,
551+
node: descriptor.node,
552+
message: interpolate(computedMessage, descriptor.data),
553+
messageId: descriptor.messageId,
554+
loc: descriptor.loc
555+
? normalizeReportLoc(descriptor)
556+
: this.#sourceCode.getLoc(descriptor.node),
557+
fix: this.#disableFixes
558+
? null
559+
: normalizeFixes(descriptor, this.#sourceCode),
560+
suggestions: this.#disableFixes
561+
? []
562+
: mapSuggestions(descriptor, this.#sourceCode, messages),
563+
language: this.#language,
564+
}),
565+
);
393566

394-
validateSuggestions(descriptor.suggest, messages);
567+
return this.messages.at(-1);
568+
}
395569

396-
return createProblem({
397-
ruleId: metadata.ruleId,
398-
severity: metadata.severity,
399-
node: descriptor.node,
400-
message: interpolate(computedMessage, descriptor.data),
401-
messageId: descriptor.messageId,
402-
loc: descriptor.loc
403-
? normalizeReportLoc(descriptor)
404-
: sourceCode.getLoc(descriptor.node),
405-
fix: metadata.disableFixes
406-
? null
407-
: normalizeFixes(descriptor, sourceCode),
408-
suggestions: metadata.disableFixes
409-
? []
410-
: mapSuggestions(descriptor, sourceCode, messages),
411-
language: metadata.language,
412-
});
413-
};
570+
/**
571+
* Adds an error message to the report. Meant to be called outside of rules.
572+
* @param {LintProblem} descriptor The descriptor for the error message.
573+
* @returns {LintMessage} The created message object.
574+
*/
575+
addError(descriptor) {
576+
const message = createLintingProblem(descriptor, 2, this.#language);
577+
this.messages.push(message);
578+
return message;
579+
}
580+
581+
/**
582+
* Adds a fatal error message to the report. Meant to be called outside of rules.
583+
* @param {LintProblem} descriptor The descriptor for the fatal error message.
584+
* @returns {LintMessage} The created message object.
585+
*/
586+
addFatal(descriptor) {
587+
const message = createLintingProblem(descriptor, 2, this.#language);
588+
message.fatal = true;
589+
this.messages.push(message);
590+
return message;
591+
}
592+
593+
/**
594+
* Adds a warning message to the report. Meant to be called outside of rules.
595+
* @param {LintProblem} descriptor The descriptor for the warning message.
596+
* @returns {LintMessage} The created message object.
597+
*/
598+
addWarning(descriptor) {
599+
const message = createLintingProblem(descriptor, 1, this.#language);
600+
this.messages.push(message);
601+
return message;
602+
}
603+
}
604+
605+
module.exports = {
606+
FileReport,
607+
updateLocationInformation,
414608
};

0 commit comments

Comments
 (0)