Skip to content

Commit 3777b77

Browse files
uniqueiniquityJamesHenry
authored andcommitted
feat(typescript-estree)!: throw error on file not in project when project set (#760)
BREAKING CHANGE: by default we will now throw when a file is not in the `project` provided
1 parent 4496288 commit 3777b77

File tree

24 files changed

+173
-50
lines changed

24 files changed

+173
-50
lines changed

.prettierignore

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
**/tests/fixtures/**/*
2+
**/tests/fixture-project/**/*
23
**/dist
34
**/coverage
45
**/shared-fixtures
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
var foo = true;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
throw 'should be ok because rule is not loaded';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
throw 'err'; // no-string-throw
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
var foo = true // semicolon
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
var foo = true; // fail
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
foo;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{}

packages/eslint-plugin-tslint/tests/index.spec.ts

+20-11
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ const ruleTester = new TSESLint.RuleTester({
1212
* Project is needed to generate the parserServices
1313
* within @typescript-eslint/parser
1414
*/
15-
project: './tests/tsconfig.json',
15+
project: './tests/fixture-project/tsconfig.json',
1616
},
1717
parser: require.resolve('@typescript-eslint/parser'),
1818
});
@@ -47,6 +47,7 @@ ruleTester.run('tslint/config', rule, {
4747
{
4848
code: 'var foo = true;',
4949
options: tslintRulesConfig,
50+
filename: './tests/fixture-project/1.ts',
5051
},
5152
{
5253
filename: './tests/test-project/file-spec.ts',
@@ -62,13 +63,15 @@ ruleTester.run('tslint/config', rule, {
6263
{
6364
code: 'throw "should be ok because rule is not loaded";',
6465
options: tslintRulesConfig,
66+
filename: './tests/fixture-project/2.ts',
6567
},
6668
],
6769

6870
invalid: [
6971
{
7072
options: [{ lintFile: './tests/test-project/tslint.json' }],
7173
code: 'throw "err" // no-string-throw',
74+
filename: './tests/fixture-project/3.ts',
7275
errors: [
7376
{
7477
messageId: 'failure',
@@ -84,6 +87,7 @@ ruleTester.run('tslint/config', rule, {
8487
code: 'var foo = true // semicolon',
8588
options: tslintRulesConfig,
8689
output: 'var foo = true // semicolon',
90+
filename: './tests/fixture-project/4.ts',
8791
errors: [
8892
{
8993
messageId: 'failure',
@@ -100,6 +104,7 @@ ruleTester.run('tslint/config', rule, {
100104
code: 'var foo = true // fail',
101105
options: tslintRulesDirectoryConfig,
102106
output: 'var foo = true // fail',
107+
filename: './tests/fixture-project/5.ts',
103108
errors: [
104109
{
105110
messageId: 'failure',
@@ -174,26 +179,30 @@ describe('tslint/error', () => {
174179
});
175180
});
176181

177-
it('should not crash if there is no tslint rules specified', () => {
182+
it('should not crash if there are no tslint rules specified', () => {
178183
const linter = new TSESLint.Linter();
179184
jest.spyOn(console, 'warn').mockImplementation();
180185
linter.defineRule('tslint/config', rule);
181186
linter.defineParser('@typescript-eslint/parser', parser);
182187
expect(() =>
183-
linter.verify('foo;', {
184-
parserOptions: {
185-
project: `${__dirname}/test-project/tsconfig.json`,
186-
},
187-
rules: {
188-
'tslint/config': [2, {}],
188+
linter.verify(
189+
'foo;',
190+
{
191+
parserOptions: {
192+
project: `${__dirname}/test-project/tsconfig.json`,
193+
},
194+
rules: {
195+
'tslint/config': [2, {}],
196+
},
197+
parser: '@typescript-eslint/parser',
189198
},
190-
parser: '@typescript-eslint/parser',
191-
}),
199+
`${__dirname}/test-project/extra.ts`,
200+
),
192201
).not.toThrow();
193202

194203
expect(console.warn).toHaveBeenCalledWith(
195204
expect.stringContaining(
196-
'Tried to lint <input> but found no valid, enabled rules for this file type and file path in the resolved configuration.',
205+
`Tried to lint ${__dirname}/test-project/extra.ts but found no valid, enabled rules for this file type and file path in the resolved configuration.`,
197206
),
198207
);
199208
jest.resetAllMocks();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
foo;

packages/eslint-plugin/tests/RuleTester.ts

+25
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ type RuleTesterConfig = Omit<TSESLint.RuleTesterConfig, 'parser'> & {
77
parser: typeof parser;
88
};
99
class RuleTester extends TSESLint.RuleTester {
10+
private filename: string | undefined = undefined;
11+
1012
// as of eslint 6 you have to provide an absolute path to the parser
1113
// but that's not as clean to type, this saves us trying to manually enforce
1214
// that contributors require.resolve everything
@@ -15,6 +17,10 @@ class RuleTester extends TSESLint.RuleTester {
1517
...options,
1618
parser: require.resolve(options.parser),
1719
});
20+
21+
if (options.parserOptions && options.parserOptions.project) {
22+
this.filename = path.join(getFixturesRootDir(), 'file.ts');
23+
}
1824
}
1925

2026
// as of eslint 6 you have to provide an absolute path to the parser
@@ -26,17 +32,36 @@ class RuleTester extends TSESLint.RuleTester {
2632
tests: TSESLint.RunTests<TMessageIds, TOptions>,
2733
): void {
2834
const errorMessage = `Do not set the parser at the test level unless you want to use a parser other than ${parser}`;
35+
36+
if (this.filename) {
37+
tests.valid = tests.valid.map(test => {
38+
if (typeof test === 'string') {
39+
return {
40+
code: test,
41+
filename: this.filename,
42+
};
43+
}
44+
return test;
45+
});
46+
}
47+
2948
tests.valid.forEach(test => {
3049
if (typeof test !== 'string') {
3150
if (test.parser === parser) {
3251
throw new Error(errorMessage);
3352
}
53+
if (!test.filename) {
54+
test.filename = this.filename;
55+
}
3456
}
3557
});
3658
tests.invalid.forEach(test => {
3759
if (test.parser === parser) {
3860
throw new Error(errorMessage);
3961
}
62+
if (!test.filename) {
63+
test.filename = this.filename;
64+
}
4065
});
4166

4267
super.run(name, rule, tests);

packages/eslint-plugin/tests/fixtures/file.ts

Whitespace-only changes.

packages/eslint-plugin/tests/rules/no-unnecessary-qualifier.test.ts

-1
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,6 @@ import * as Foo from './foo';
199199
declare module './foo' {
200200
const x: Foo.T = 3;
201201
}`,
202-
filename: path.join(rootPath, 'bar.ts'),
203202
errors: [
204203
{
205204
messageId,

packages/parser/README.md

+15
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,23 @@ The following additional configuration options are available by specifying them
5050

5151
- **`project`** - default `undefined`. This option allows you to provide a path to your project's `tsconfig.json`. **This setting is required if you want to use rules which require type information**. You may want to use this setting in tandem with the `tsconfigRootDir` option below.
5252

53+
- Note that if this setting is specified and `createDefaultProgram` is not, you must only lint files that are included in the projects as defined by the provided `tsconfig.json` files. If your existing configuration does not include all of the files you would like to lint, you can create a separate `tsconfig.eslint.json` as follows:
54+
55+
```ts
56+
{
57+
"extends": "./tsconfig.json", // path to existing tsconfig
58+
"includes": [
59+
"src/**/*.ts",
60+
"test/**/*.ts",
61+
// etc
62+
]
63+
}
64+
```
65+
5366
- **`tsconfigRootDir`** - default `undefined`. This option allows you to provide the root directory for relative tsconfig paths specified in the `project` option above.
5467

68+
- **`createDefaultProgram`** - default `false`. This option allows you to request that when the `project` setting is specified, files will be allowed when not included in the projects defined by the provided `tsconfig.json` files. However, this may incur significant performance costs, so this option is primarily included for backwards-compatibility. See the **`project`** section for more information.
69+
5570
- **`extraFileExtensions`** - default `undefined`. This option allows you to provide one or more additional file extensions which should be considered in the TypeScript Program compilation. E.g. a `.vue` file
5671

5772
- **`warnOnUnsupportedTypeScriptVersion`** - default `true`. This option allows you to toggle the warning that the parser will give you if you use a version of TypeScript which is not explicitly supported

packages/parser/tests/lib/parser.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,12 @@ describe('parser', () => {
5050
jsx: false,
5151
},
5252
// ts-estree specific
53-
filePath: 'test/foo',
53+
filePath: 'tests/fixtures/services/isolated-file.src.ts',
5454
project: 'tsconfig.json',
5555
useJSXTextNode: false,
5656
errorOnUnknownASTType: false,
5757
errorOnTypeScriptSyntacticAndSemanticIssues: false,
58-
tsconfigRootDir: './',
58+
tsconfigRootDir: 'tests/fixtures/services',
5959
extraFileExtensions: ['foo'],
6060
};
6161
parseForESLint(code, config);

packages/typescript-estree/src/parser-options.ts

+2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export interface Extra {
1818
tsconfigRootDir: string;
1919
extraFileExtensions: string[];
2020
preserveNodeMaps?: boolean;
21+
createDefaultProgram: boolean;
2122
}
2223

2324
export interface TSESTreeOptions {
@@ -35,6 +36,7 @@ export interface TSESTreeOptions {
3536
tsconfigRootDir?: string;
3637
extraFileExtensions?: string[];
3738
preserveNodeMaps?: boolean;
39+
createDefaultProgram?: boolean;
3840
}
3941

4042
// This lets us use generics to type the return value, and removes the need to

packages/typescript-estree/src/parser.ts

+29-12
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ function resetExtra(): void {
5858
tsconfigRootDir: process.cwd(),
5959
extraFileExtensions: [],
6060
preserveNodeMaps: undefined,
61+
createDefaultProgram: false,
6162
};
6263
}
6364

@@ -66,20 +67,27 @@ function resetExtra(): void {
6667
* @param options The config object
6768
* @returns If found, returns the source file corresponding to the code and the containing program
6869
*/
69-
function getASTFromProject(code: string, options: TSESTreeOptions) {
70-
return firstDefined(
71-
calculateProjectParserOptions(
72-
code,
73-
options.filePath || getFileName(options),
74-
extra,
75-
),
70+
function getASTFromProject(
71+
code: string,
72+
options: TSESTreeOptions,
73+
createDefaultProgram: boolean,
74+
) {
75+
const filePath = options.filePath || getFileName(options);
76+
const astAndProgram = firstDefined(
77+
calculateProjectParserOptions(code, filePath, extra),
7678
currentProgram => {
77-
const ast = currentProgram.getSourceFile(
78-
options.filePath || getFileName(options),
79-
);
79+
const ast = currentProgram.getSourceFile(filePath);
8080
return ast && { ast, program: currentProgram };
8181
},
8282
);
83+
84+
if (!astAndProgram && !createDefaultProgram) {
85+
throw new Error(
86+
`If "parserOptions.project" has been set for @typescript-eslint/parser, ${filePath} must be included in at least one of the projects provided.`,
87+
);
88+
}
89+
90+
return astAndProgram;
8391
}
8492

8593
/**
@@ -161,10 +169,14 @@ function getProgramAndAST(
161169
code: string,
162170
options: TSESTreeOptions,
163171
shouldProvideParserServices: boolean,
172+
createDefaultProgram: boolean,
164173
) {
165174
return (
166-
(shouldProvideParserServices && getASTFromProject(code, options)) ||
167-
(shouldProvideParserServices && getASTAndDefaultProject(code, options)) ||
175+
(shouldProvideParserServices &&
176+
getASTFromProject(code, options, createDefaultProgram)) ||
177+
(shouldProvideParserServices &&
178+
createDefaultProgram &&
179+
getASTAndDefaultProject(code, options)) ||
168180
createNewProgram(code)
169181
);
170182
}
@@ -254,6 +266,10 @@ function applyParserOptionsToExtra(options: TSESTreeOptions): void {
254266
if (options.preserveNodeMaps === undefined && extra.projects.length > 0) {
255267
extra.preserveNodeMaps = true;
256268
}
269+
270+
extra.createDefaultProgram =
271+
typeof options.createDefaultProgram === 'boolean' &&
272+
options.createDefaultProgram;
257273
}
258274

259275
function warnAboutTSVersion(): void {
@@ -386,6 +402,7 @@ export function parseAndGenerateServices<
386402
code,
387403
options,
388404
shouldProvideParserServices,
405+
extra.createDefaultProgram,
389406
);
390407
/**
391408
* Determine whether or not two-way maps of converted AST nodes should be preserved

packages/typescript-estree/src/tsconfig-parser.ts

+10
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,16 @@ const watchCallbackTrackingMap = new Map<string, ts.FileWatcherCallback>();
3030

3131
const parsedFilesSeen = new Set<string>();
3232

33+
/**
34+
* Clear tsconfig caches.
35+
* Primarily used for testing.
36+
*/
37+
export function clearCaches() {
38+
knownWatchProgramMap.clear();
39+
watchCallbackTrackingMap.clear();
40+
parsedFilesSeen.clear();
41+
}
42+
3343
/**
3444
* Holds information about the file currently being linted
3545
*/

packages/typescript-estree/tests/fixtures/simpleProject/file.ts

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{}

0 commit comments

Comments
 (0)