1
1
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
2
2
// See LICENSE in the project root for license information.
3
3
4
- import * as path from 'path' ;
5
4
import * as crypto from 'crypto' ;
6
5
import * as semver from 'semver' ;
7
6
import type * as TTypescript from 'typescript' ;
@@ -26,8 +25,10 @@ enum EslintMessageSeverity {
26
25
error = 2
27
26
}
28
27
28
+ // Patch the timer used to track rule execution time. This allows us to get access to the detailed information
29
+ // about how long each rule took to execute, which we provide on the CLI when running in verbose mode.
29
30
async function patchTimerAsync ( eslintPackagePath : string , timingsMap : Map < string , number > ) : Promise < void > {
30
- const timingModulePath : string = path . join ( eslintPackagePath , ' lib' , ' linter' , ' timing' ) ;
31
+ const timingModulePath : string = ` ${ eslintPackagePath } / lib/ linter/ timing` ;
31
32
const timing : IEslintTiming = ( await import ( timingModulePath ) ) . default ;
32
33
timing . enabled = true ;
33
34
const patchedTime : ( key : string , fn : ( ...args : unknown [ ] ) => unknown ) => ( ...args : unknown [ ] ) => unknown = (
@@ -46,26 +47,55 @@ async function patchTimerAsync(eslintPackagePath: string, timingsMap: Map<string
46
47
timing . time = patchedTime ;
47
48
}
48
49
50
+ function getFormattedErrorMessage ( lintMessage : TEslint . Linter . LintMessage ) : string {
51
+ // https://eslint.org/docs/developer-guide/nodejs-api#◆-lintmessage-type
52
+ return lintMessage . ruleId ? `(${ lintMessage . ruleId } ) ${ lintMessage . message } ` : lintMessage . message ;
53
+ }
54
+
49
55
export class Eslint extends LinterBase < TEslint . ESLint . LintResult > {
50
56
private readonly _eslintPackage : typeof TEslint ;
51
57
private readonly _linter : TEslint . ESLint ;
52
58
private readonly _eslintTimings : Map < string , number > = new Map ( ) ;
59
+ private readonly _currentFixMessages : TEslint . Linter . LintMessage [ ] = [ ] ;
60
+ private readonly _fixMessagesByResult : Map < TEslint . ESLint . LintResult , TEslint . Linter . LintMessage [ ] > =
61
+ new Map ( ) ;
53
62
54
63
protected constructor ( options : IEslintOptions ) {
55
64
super ( 'eslint' , options ) ;
56
65
57
- const { buildFolderPath, eslintPackage, linterConfigFilePath, tsProgram, eslintTimings } = options ;
66
+ const { buildFolderPath, eslintPackage, linterConfigFilePath, tsProgram, eslintTimings, fix } = options ;
58
67
this . _eslintPackage = eslintPackage ;
59
- this . _linter = new eslintPackage . ESLint ( {
60
- cwd : buildFolderPath ,
61
- overrideConfigFile : linterConfigFilePath ,
62
- // Override config takes precedence over overrideConfigFile, which allows us to provide
63
- // the source TypeScript program to ESLint
64
- overrideConfig : {
68
+
69
+ let overrideConfig : TEslint . Linter . Config | undefined ;
70
+ let fixFn : Exclude < TEslint . ESLint . Options [ 'fix' ] , boolean > ;
71
+ if ( fix ) {
72
+ // We do not recieve the messages for the issues that were fixed, so we need to track them ourselves
73
+ // so that we can log them after the fix is applied. This array will be populated by the fix function,
74
+ // and subsequently mapped to the results in the ESLint.lintFileAsync method below. After the messages
75
+ // are mapped, the array will be cleared so that it is ready for the next fix operation.
76
+ fixFn = ( message : TEslint . Linter . LintMessage ) => {
77
+ this . _currentFixMessages . push ( message ) ;
78
+ return true ;
79
+ } ;
80
+ } else {
81
+ // The @typescript -eslint/parser package allows providing an existing TypeScript program to avoid needing
82
+ // to reparse. However, fixers in ESLint run in multiple passes against the underlying code until the
83
+ // fix fully succeeds. This conflicts with providing an existing program as the code no longer maps to
84
+ // the provided program, producing garbage fix output. To avoid this, only provide the existing program
85
+ // if we're not fixing.
86
+ overrideConfig = {
65
87
parserOptions : {
66
88
programs : [ tsProgram ]
67
89
}
68
- }
90
+ } ;
91
+ }
92
+
93
+ this . _linter = new eslintPackage . ESLint ( {
94
+ cwd : buildFolderPath ,
95
+ overrideConfigFile : linterConfigFilePath ,
96
+ // Override config takes precedence over overrideConfigFile
97
+ overrideConfig,
98
+ fix : fixFn
69
99
} ) ;
70
100
this . _eslintTimings = eslintTimings ;
71
101
}
@@ -121,17 +151,32 @@ export class Eslint extends LinterBase<TEslint.ESLint.LintResult> {
121
151
filePath : sourceFile . fileName
122
152
} ) ;
123
153
154
+ // Map the fix messages to the results. This API should only return one result per file, so we can be sure
155
+ // that the fix messages belong to the returned result. If we somehow receive multiple results, we will
156
+ // drop the messages on the floor, but since they are only used for logging, this should not be a problem.
157
+ const fixMessages : TEslint . Linter . LintMessage [ ] = this . _currentFixMessages . splice ( 0 ) ;
158
+ if ( lintResults . length === 1 ) {
159
+ this . _fixMessagesByResult . set ( lintResults [ 0 ] , fixMessages ) ;
160
+ }
161
+
162
+ this . _fixesPossible =
163
+ this . _fixesPossible ||
164
+ ( ! this . _fix &&
165
+ lintResults . some ( ( lintResult : TEslint . ESLint . LintResult ) => {
166
+ return lintResult . fixableErrorCount + lintResult . fixableWarningCount > 0 ;
167
+ } ) ) ;
168
+
124
169
const failures : TEslint . ESLint . LintResult [ ] = [ ] ;
125
170
for ( const lintResult of lintResults ) {
126
- if ( lintResult . messages . length > 0 ) {
171
+ if ( lintResult . messages . length > 0 || lintResult . output ) {
127
172
failures . push ( lintResult ) ;
128
173
}
129
174
}
130
175
131
176
return failures ;
132
177
}
133
178
134
- protected lintingFinished ( lintFailures : TEslint . ESLint . LintResult [ ] ) : void {
179
+ protected async lintingFinishedAsync ( lintFailures : TEslint . ESLint . LintResult [ ] ) : Promise < void > {
135
180
let omittedRuleCount : number = 0 ;
136
181
const timings : [ string , number ] [ ] = Array . from ( this . _eslintTimings ) . sort (
137
182
( x : [ string , number ] , y : [ string , number ] ) => {
@@ -150,45 +195,58 @@ export class Eslint extends LinterBase<TEslint.ESLint.LintResult> {
150
195
this . _terminal . writeVerboseLine ( `${ omittedRuleCount } rules took 0ms` ) ;
151
196
}
152
197
153
- const errors : Error [ ] = [ ] ;
154
- const warnings : Error [ ] = [ ] ;
155
-
156
- for ( const eslintFailure of lintFailures ) {
157
- for ( const message of eslintFailure . messages ) {
158
- // https://eslint.org/docs/developer-guide/nodejs-api#◆-lintmessage-type
159
- const formattedMessage : string = message . ruleId
160
- ? `(${ message . ruleId } ) ${ message . message } `
161
- : message . message ;
162
- const errorObject : FileError = new FileError ( formattedMessage , {
163
- absolutePath : eslintFailure . filePath ,
164
- projectFolder : this . _buildFolderPath ,
165
- line : message . line ,
166
- column : message . column
167
- } ) ;
168
- switch ( message . severity ) {
198
+ if ( this . _fix && this . _fixMessagesByResult . size > 0 ) {
199
+ await this . _eslintPackage . ESLint . outputFixes ( lintFailures ) ;
200
+ }
201
+
202
+ for ( const lintFailure of lintFailures ) {
203
+ // Report linter fixes to the logger. These will only be returned when the underlying failure was fixed
204
+ const fixMessages : TEslint . Linter . LintMessage [ ] | undefined =
205
+ this . _fixMessagesByResult . get ( lintFailure ) ;
206
+ if ( fixMessages ) {
207
+ for ( const fixMessage of fixMessages ) {
208
+ const formattedMessage : string = `[FIXED] ${ getFormattedErrorMessage ( fixMessage ) } ` ;
209
+ const errorObject : FileError = this . _getLintFileError ( lintFailure , fixMessage , formattedMessage ) ;
210
+ this . _scopedLogger . emitWarning ( errorObject ) ;
211
+ }
212
+ }
213
+
214
+ // Report linter errors and warnings to the logger
215
+ for ( const lintMessage of lintFailure . messages ) {
216
+ const errorObject : FileError = this . _getLintFileError ( lintFailure , lintMessage ) ;
217
+ switch ( lintMessage . severity ) {
169
218
case EslintMessageSeverity . error : {
170
- errors . push ( errorObject ) ;
219
+ this . _scopedLogger . emitError ( errorObject ) ;
171
220
break ;
172
221
}
173
222
174
223
case EslintMessageSeverity . warning : {
175
- warnings . push ( errorObject ) ;
224
+ this . _scopedLogger . emitWarning ( errorObject ) ;
176
225
break ;
177
226
}
178
227
}
179
228
}
180
229
}
181
-
182
- for ( const error of errors ) {
183
- this . _scopedLogger . emitError ( error ) ;
184
- }
185
-
186
- for ( const warning of warnings ) {
187
- this . _scopedLogger . emitWarning ( warning ) ;
188
- }
189
230
}
190
231
191
232
protected async isFileExcludedAsync ( filePath : string ) : Promise < boolean > {
192
233
return await this . _linter . isPathIgnored ( filePath ) ;
193
234
}
235
+
236
+ private _getLintFileError (
237
+ lintResult : TEslint . ESLint . LintResult ,
238
+ lintMessage : TEslint . Linter . LintMessage ,
239
+ message ?: string
240
+ ) : FileError {
241
+ if ( ! message ) {
242
+ message = getFormattedErrorMessage ( lintMessage ) ;
243
+ }
244
+
245
+ return new FileError ( message , {
246
+ absolutePath : lintResult . filePath ,
247
+ projectFolder : this . _buildFolderPath ,
248
+ line : lintMessage . line ,
249
+ column : lintMessage . column
250
+ } ) ;
251
+ }
194
252
}
0 commit comments