55
66require ( 'util' ) . inspect . defaultOptions . depth = null ;
77
8- const { join, normalize , sep } = require ( 'path' ) ;
8+ const { join, dirname } = require ( 'path' ) ;
99const { createRequire } = require ( 'module' ) ;
1010const compareVersions = require ( 'compare-versions' ) ;
1111const ndjson = require ( 'ndjson' ) ;
1212
13- const PATHS_CACHE = { } ;
14- const ESLINT_CACHE = { } ;
15-
1613const MINIMUM_ESLINT_VERSION = '7.0.0' ;
1714
15+ const PATHS_CACHE = new Map ( ) ;
16+ const ESLINT_CACHE = new Map ( ) ;
17+
18+
1819class IncompatibleVersionError extends Error {
1920 constructor ( version ) {
2021 // eslint-disable-next-line max-len
@@ -56,39 +57,28 @@ function log (message) {
5657 emit ( { log : message } ) ;
5758}
5859
59- function getPathRoot ( filePath , eslintPath ) {
60- log ( `getPathRoot ${ filePath } // ${ eslintPath } ` ) ;
61- filePath = normalize ( filePath ) . split ( sep ) ;
62- eslintPath = normalize ( eslintPath ) . split ( sep ) ;
63-
64- for ( let index in filePath ) {
65- if ( eslintPath [ index ] !== filePath [ index ] ) {
66- let ret = filePath . slice ( 0 , index ) . join ( sep ) ;
67- return ret + sep ;
68- }
69- }
70-
71- throw new Error ( 'linter-eslint-node: Cannot determine root' ) ;
72- }
73-
74- function buildCommonConstructorOptions ( config ) {
60+ function buildCommonConstructorOptions ( config , cwd ) {
7561 let {
7662 advanced : { disableEslintIgnore } ,
7763 autofix : { rulesToDisableWhileFixing }
7864 } = config ;
7965
8066 return {
67+ cwd,
8168 ignore : ! disableEslintIgnore ,
69+ // `fix` can be a function, so we'll use it to ignore any rules that the
70+ // user has told us to ignore. This isn't a "common" option, but it's easy
71+ // to overwrite with `fix: false` for the lint-only instance.
8272 fix : ( { ruleId } ) => ! rulesToDisableWhileFixing . includes ( ruleId )
8373 } ;
8474}
8575
8676function clearESLintCache ( ) {
87- for ( let key in PATHS_CACHE ) {
88- delete PATHS_CACHE [ key ] ;
77+ for ( let key of PATHS_CACHE ) {
78+ PATHS_CACHE . delete ( key ) ;
8979 }
90- for ( let key in ESLINT_CACHE ) {
91- delete ESLINT_CACHE [ key ] ;
80+ for ( let key of ESLINT_CACHE ) {
81+ ESLINT_CACHE . delete ( key ) ;
9282 }
9383}
9484
@@ -100,52 +90,72 @@ function resolveESLint (filePath) {
10090 }
10191}
10292
103- function getESLint ( filePath , projectPath , config , { legacyPackagePresent } ) {
104- if ( ! PATHS_CACHE [ filePath ] ) {
105- PATHS_CACHE [ filePath ] = resolveESLint ( filePath ) ;
93+ let builtInEslintPath ;
94+ function resolveBuiltInESLint ( ) {
95+ if ( ! builtInEslintPath ) {
96+ builtInEslintPath = createRequire ( __dirname ) . resolve ( 'eslint' ) ;
10697 }
98+ return builtInEslintPath ;
99+ }
107100
108- let eslintPath = PATHS_CACHE [ filePath ] ;
101+ function getESLint ( filePath , config , { legacyPackagePresent } ) {
102+ let resolveDir = dirname ( filePath ) ;
103+ if ( ! PATHS_CACHE . has ( resolveDir ) ) {
104+ PATHS_CACHE . set ( resolveDir , resolveESLint ( filePath ) ) ;
105+ }
109106
110- if ( ! ESLINT_CACHE [ eslintPath ] ) {
107+ let eslintPath = PATHS_CACHE . get ( resolveDir ) ;
108+
109+ // We need to manage an arbitrary number of different ESLint versions without
110+ // having them clash. On top of that, a declaration of `ESLint` accepts a
111+ // `cwd` option on instantiation and infers its configuration based on the
112+ // `.eslintrc`s and `.eslintignore`s that it sees from that particular
113+ // directory.
114+ //
115+ // Thus an `ESLint` instance is good for the directory it was created in and
116+ // nothing more. If two files are siblings in a project's file tree, linting
117+ // the first one will create an instance that can be reused when we lint the
118+ // second, but can't be reused with any file in the parent directory or in
119+ // any child directories.
120+ //
121+ // TODO: Add an option to disable this method of caching. Could be useful as
122+ // a diagnostic tool and as a workaround when users report bugs.
123+ if ( ! ESLINT_CACHE . has ( resolveDir ) ) {
111124 const eslintRootPath = eslintPath . replace ( / e s l i n t ( [ / \\ ] ) .* ?$ / , 'eslint$1' ) ;
112125 const packageMeta = require ( join ( eslintRootPath , 'package.json' ) ) ;
113126
114- let { ESLint } = createRequire ( eslintPath ) ( 'eslint' ) ;
115- let commonOptions = buildCommonConstructorOptions ( config ) ;
127+ const { ESLint } = createRequire ( eslintPath ) ( 'eslint' ) ;
128+ let commonOptions = buildCommonConstructorOptions ( config , resolveDir ) ;
129+
130+ const eslintLint = new ESLint ( { ...commonOptions , fix : false } ) ;
131+ const eslintFix = new ESLint ( { ...commonOptions } ) ;
116132
117- // `fix` is a predicate in `commonOptions` and represents the "yes,
118- // except..." outcome. For the linter version, we overwrite it with `fix:
119- // false`.
120- ESLINT_CACHE [ eslintPath ] = {
133+ ESLINT_CACHE . set ( resolveDir , {
121134 ESLint,
122- eslintLint : new ESLint ( { ...commonOptions , fix : false } ) ,
123- eslintFix : new ESLint ( { ...commonOptions } ) ,
124- workingDir : getPathRoot ( filePath , eslintPath ) ,
135+ eslintLint,
136+ eslintFix,
125137 eslintPath : eslintRootPath ,
126138 eslintVersion : packageMeta . version ,
127- isBuiltIn : eslintPath === PATHS_CACHE [ __dirname ]
128- } ;
139+ isBuiltIn : eslintPath === resolveBuiltInESLint ( )
140+ } ) ;
129141 }
130142
131- let cache = ESLINT_CACHE [ eslintPath ] ;
143+ let cached = ESLINT_CACHE . get ( resolveDir ) ;
132144
133- if ( compareVersions ( cache . eslintVersion , MINIMUM_ESLINT_VERSION ) < 1 ) {
145+ if ( compareVersions ( cached . eslintVersion , MINIMUM_ESLINT_VERSION ) < 1 ) {
134146 // Unsupported version.
135- throw new IncompatibleVersionError ( cache . eslintVersion ) ;
136- } else if ( ( compareVersions ( cache . eslintVersion , '8.0.0' ) < 1 ) && legacyPackagePresent ) {
147+ throw new IncompatibleVersionError ( cached . eslintVersion ) ;
148+ } else if ( ( compareVersions ( cached . eslintVersion , '8.0.0' ) < 1 ) && legacyPackagePresent ) {
137149 // We're dealing with version 7 of ESLint. The legacy `linter-eslint`
138150 // package is present and capable of linting with this version, so we
139151 // should halt instead of trying to lint everything twice.
140- throw new VersionOverlapError ( cache . eslintVersion ) ;
152+ throw new VersionOverlapError ( cached . eslintVersion ) ;
141153 }
142154
143- return ESLINT_CACHE [ eslintPath ] ;
155+ return cached ;
144156}
145157
146158async function lint ( eslint , workingDir , filePath , fileContent ) {
147- process . chdir ( workingDir ) ;
148-
149159 if ( typeof fileContent === 'string' ) {
150160 return eslint . lintText ( fileContent , { filePath } ) ;
151161 } else {
@@ -235,7 +245,6 @@ async function processMessage (bundle) {
235245 isModified,
236246 key,
237247 legacyPackagePresent,
238- projectPath,
239248 type
240249 } = bundle ;
241250
@@ -258,18 +267,9 @@ async function processMessage (bundle) {
258267 return ;
259268 }
260269
261- // if (!projectPath) {
262- // emit({
263- // key,
264- // error: `Must provide projectPath`,
265- // type: 'no-project'
266- // });
267- // return;
268- // }
269-
270270 let eslint ;
271271 try {
272- eslint = getESLint ( filePath , projectPath , config , { legacyPackagePresent } ) ;
272+ eslint = getESLint ( filePath , config , { legacyPackagePresent } ) ;
273273 } catch ( err ) {
274274 if ( err instanceof IncompatibleVersionError ) {
275275 emit ( {
0 commit comments