Thanks to visit codestin.com
Credit goes to github.com

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)