Skip to content

Commit ed5564d

Browse files
authored
feat(typescript-estree): support long running lint without watch (typescript-eslint#1106)
1 parent 0c85ac3 commit ed5564d

File tree

15 files changed

+882
-700
lines changed

15 files changed

+882
-700
lines changed

.vscode/launch.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,22 @@
3535
"sourceMaps": true,
3636
"console": "integratedTerminal",
3737
"internalConsoleOptions": "neverOpen"
38+
},
39+
{
40+
"type": "node",
41+
"request": "launch",
42+
"name": "Run currently opened parser test",
43+
"cwd": "${workspaceFolder}/packages/parser/",
44+
"program": "${workspaceFolder}/node_modules/jest/bin/jest.js",
45+
"args": [
46+
"--runInBand",
47+
"--no-cache",
48+
"--no-coverage",
49+
"${relativeFile}"
50+
],
51+
"sourceMaps": true,
52+
"console": "integratedTerminal",
53+
"internalConsoleOptions": "neverOpen"
3854
}
3955
]
4056
}

packages/eslint-plugin/tests/RuleTester.ts

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,30 @@ type RuleTesterConfig = Omit<TSESLint.RuleTesterConfig, 'parser'> & {
88
parser: typeof parser;
99
};
1010
class RuleTester extends TSESLint.RuleTester {
11-
private filename: string | undefined = undefined;
12-
1311
// as of eslint 6 you have to provide an absolute path to the parser
1412
// but that's not as clean to type, this saves us trying to manually enforce
1513
// that contributors require.resolve everything
16-
constructor(options: RuleTesterConfig) {
14+
constructor(private readonly options: RuleTesterConfig) {
1715
super({
1816
...options,
1917
parser: require.resolve(options.parser),
2018
});
19+
}
20+
private getFilename(options?: TSESLint.ParserOptions): string {
21+
if (options) {
22+
const filename = `file.ts${
23+
options.ecmaFeatures && options.ecmaFeatures.jsx ? 'x' : ''
24+
}`;
25+
if (options.project) {
26+
return path.join(getFixturesRootDir(), filename);
27+
}
2128

22-
if (options.parserOptions && options.parserOptions.project) {
23-
this.filename = path.join(getFixturesRootDir(), 'file.ts');
29+
return filename;
30+
} else if (this.options.parserOptions) {
31+
return this.getFilename(this.options.parserOptions);
2432
}
33+
34+
return 'file.ts';
2535
}
2636

2737
// as of eslint 6 you have to provide an absolute path to the parser
@@ -34,25 +44,22 @@ class RuleTester extends TSESLint.RuleTester {
3444
): void {
3545
const errorMessage = `Do not set the parser at the test level unless you want to use a parser other than ${parser}`;
3646

37-
if (this.filename) {
38-
tests.valid = tests.valid.map(test => {
39-
if (typeof test === 'string') {
40-
return {
41-
code: test,
42-
filename: this.filename,
43-
};
44-
}
45-
return test;
46-
});
47-
}
47+
tests.valid = tests.valid.map(test => {
48+
if (typeof test === 'string') {
49+
return {
50+
code: test,
51+
};
52+
}
53+
return test;
54+
});
4855

4956
tests.valid.forEach(test => {
5057
if (typeof test !== 'string') {
5158
if (test.parser === parser) {
5259
throw new Error(errorMessage);
5360
}
5461
if (!test.filename) {
55-
test.filename = this.filename;
62+
test.filename = this.getFilename(test.parserOptions);
5663
}
5764
}
5865
});
@@ -61,7 +68,7 @@ class RuleTester extends TSESLint.RuleTester {
6168
throw new Error(errorMessage);
6269
}
6370
if (!test.filename) {
64-
test.filename = this.filename;
71+
test.filename = this.getFilename(test.parserOptions);
6572
}
6673
});
6774

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import debug from 'debug';
2+
import path from 'path';
3+
import ts from 'typescript';
4+
import { Extra } from '../parser-options';
5+
import {
6+
getTsconfigPath,
7+
DEFAULT_COMPILER_OPTIONS,
8+
ASTAndProgram,
9+
} from './shared';
10+
11+
const log = debug('typescript-eslint:typescript-estree:createDefaultProgram');
12+
13+
/**
14+
* @param code The code of the file being linted
15+
* @param options The config object
16+
* @param extra.tsconfigRootDir The root directory for relative tsconfig paths
17+
* @param extra.projects Provided tsconfig paths
18+
* @returns If found, returns the source file corresponding to the code and the containing program
19+
*/
20+
function createDefaultProgram(
21+
code: string,
22+
extra: Extra,
23+
): ASTAndProgram | undefined {
24+
log('Getting default program for: %s', extra.filePath || 'unnamed file');
25+
26+
if (!extra.projects || extra.projects.length !== 1) {
27+
return undefined;
28+
}
29+
30+
const tsconfigPath = getTsconfigPath(extra.projects[0], extra);
31+
32+
const commandLine = ts.getParsedCommandLineOfConfigFile(
33+
tsconfigPath,
34+
DEFAULT_COMPILER_OPTIONS,
35+
{ ...ts.sys, onUnRecoverableConfigFileDiagnostic: () => {} },
36+
);
37+
38+
if (!commandLine) {
39+
return undefined;
40+
}
41+
42+
const compilerHost = ts.createCompilerHost(commandLine.options, true);
43+
const oldReadFile = compilerHost.readFile;
44+
compilerHost.readFile = (fileName: string): string | undefined =>
45+
path.normalize(fileName) === path.normalize(extra.filePath)
46+
? code
47+
: oldReadFile(fileName);
48+
49+
const program = ts.createProgram(
50+
[extra.filePath],
51+
commandLine.options,
52+
compilerHost,
53+
);
54+
const ast = program.getSourceFile(extra.filePath);
55+
56+
return ast && { ast, program };
57+
}
58+
59+
export { createDefaultProgram };
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import debug from 'debug';
2+
import ts from 'typescript';
3+
import { Extra } from '../parser-options';
4+
import { ASTAndProgram, DEFAULT_COMPILER_OPTIONS } from './shared';
5+
6+
const log = debug('typescript-eslint:typescript-estree:createIsolatedProgram');
7+
8+
/**
9+
* @param code The code of the file being linted
10+
* @returns Returns a new source file and program corresponding to the linted code
11+
*/
12+
function createIsolatedProgram(code: string, extra: Extra): ASTAndProgram {
13+
log('Getting isolated program for: %s', extra.filePath);
14+
15+
const compilerHost: ts.CompilerHost = {
16+
fileExists() {
17+
return true;
18+
},
19+
getCanonicalFileName() {
20+
return extra.filePath;
21+
},
22+
getCurrentDirectory() {
23+
return '';
24+
},
25+
getDirectories() {
26+
return [];
27+
},
28+
getDefaultLibFileName() {
29+
return 'lib.d.ts';
30+
},
31+
32+
// TODO: Support Windows CRLF
33+
getNewLine() {
34+
return '\n';
35+
},
36+
getSourceFile(filename: string) {
37+
return ts.createSourceFile(filename, code, ts.ScriptTarget.Latest, true);
38+
},
39+
readFile() {
40+
return undefined;
41+
},
42+
useCaseSensitiveFileNames() {
43+
return true;
44+
},
45+
writeFile() {
46+
return null;
47+
},
48+
};
49+
50+
const program = ts.createProgram(
51+
[extra.filePath],
52+
{
53+
noResolve: true,
54+
target: ts.ScriptTarget.Latest,
55+
jsx: extra.jsx ? ts.JsxEmit.Preserve : undefined,
56+
...DEFAULT_COMPILER_OPTIONS,
57+
},
58+
compilerHost,
59+
);
60+
61+
const ast = program.getSourceFile(extra.filePath);
62+
if (!ast) {
63+
throw new Error(
64+
'Expected an ast to be returned for the single-file isolated program.',
65+
);
66+
}
67+
68+
return { ast, program };
69+
}
70+
71+
export { createIsolatedProgram };
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import debug from 'debug';
2+
import path from 'path';
3+
import { getProgramsForProjects } from './createWatchProgram';
4+
import { firstDefined } from '../node-utils';
5+
import { Extra } from '../parser-options';
6+
import { ASTAndProgram } from './shared';
7+
8+
const log = debug('typescript-eslint:typescript-estree:createProjectProgram');
9+
10+
/**
11+
* @param code The code of the file being linted
12+
* @param options The config object
13+
* @returns If found, returns the source file corresponding to the code and the containing program
14+
*/
15+
function createProjectProgram(
16+
code: string,
17+
createDefaultProgram: boolean,
18+
extra: Extra,
19+
): ASTAndProgram | undefined {
20+
log('Creating project program for: %s', extra.filePath);
21+
22+
const astAndProgram = firstDefined(
23+
getProgramsForProjects(code, extra.filePath, extra),
24+
currentProgram => {
25+
const ast = currentProgram.getSourceFile(extra.filePath);
26+
return ast && { ast, program: currentProgram };
27+
},
28+
);
29+
30+
if (!astAndProgram && !createDefaultProgram) {
31+
// the file was either not matched within the tsconfig, or the extension wasn't expected
32+
const errorLines = [
33+
'"parserOptions.project" has been set for @typescript-eslint/parser.',
34+
`The file does not match your project config: ${path.relative(
35+
process.cwd(),
36+
extra.filePath,
37+
)}.`,
38+
];
39+
let hasMatchedAnError = false;
40+
41+
const fileExtension = path.extname(extra.filePath);
42+
if (!['.ts', '.tsx', '.js', '.jsx'].includes(fileExtension)) {
43+
const nonStandardExt = `The extension for the file (${fileExtension}) is non-standard`;
44+
if (extra.extraFileExtensions && extra.extraFileExtensions.length > 0) {
45+
if (!extra.extraFileExtensions.includes(fileExtension)) {
46+
errorLines.push(
47+
`${nonStandardExt}. It should be added to your existing "parserOptions.extraFileExtensions".`,
48+
);
49+
hasMatchedAnError = true;
50+
}
51+
} else {
52+
errorLines.push(
53+
`${nonStandardExt}. You should add "parserOptions.extraFileExtensions" to your config.`,
54+
);
55+
hasMatchedAnError = true;
56+
}
57+
}
58+
59+
if (!hasMatchedAnError) {
60+
errorLines.push(
61+
'The file must be included in at least one of the projects provided.',
62+
);
63+
hasMatchedAnError = true;
64+
}
65+
66+
throw new Error(errorLines.join('\n'));
67+
}
68+
69+
return astAndProgram;
70+
}
71+
72+
export { createProjectProgram };
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import debug from 'debug';
2+
import ts from 'typescript';
3+
import { Extra } from '../parser-options';
4+
5+
const log = debug('typescript-eslint:typescript-estree:createIsolatedProgram');
6+
7+
function createSourceFile(code: string, extra: Extra): ts.SourceFile {
8+
log('Getting AST without type information for: %s', extra.filePath);
9+
10+
return ts.createSourceFile(
11+
extra.filePath,
12+
code,
13+
ts.ScriptTarget.Latest,
14+
/* setParentNodes */ true,
15+
);
16+
}
17+
18+
export { createSourceFile };

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy