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" ;
1212const assert = require ( "../shared/assert" ) ;
1313const { RuleFixer } = require ( "./rule-fixer" ) ;
1414const { 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