Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(typescript-estree)!: throw error on file not in project when project set #760

Merged
1 change: 1 addition & 0 deletions .prettierignore
@@ -1,4 +1,5 @@
**/tests/fixtures/**/*
**/tests/fixture-project/**/*
**/dist
**/coverage
**/shared-fixtures
Expand Down
1 change: 1 addition & 0 deletions packages/eslint-plugin-tslint/tests/fixture-project/1.ts
@@ -0,0 +1 @@
var foo = true;
1 change: 1 addition & 0 deletions packages/eslint-plugin-tslint/tests/fixture-project/2.ts
@@ -0,0 +1 @@
throw 'should be ok because rule is not loaded';
1 change: 1 addition & 0 deletions packages/eslint-plugin-tslint/tests/fixture-project/3.ts
@@ -0,0 +1 @@
throw 'err'; // no-string-throw
1 change: 1 addition & 0 deletions packages/eslint-plugin-tslint/tests/fixture-project/4.ts
@@ -0,0 +1 @@
var foo = true // semicolon
1 change: 1 addition & 0 deletions packages/eslint-plugin-tslint/tests/fixture-project/5.ts
@@ -0,0 +1 @@
var foo = true; // fail
1 change: 1 addition & 0 deletions packages/eslint-plugin-tslint/tests/fixture-project/6.ts
@@ -0,0 +1 @@
foo;
@@ -0,0 +1 @@
{}
31 changes: 20 additions & 11 deletions packages/eslint-plugin-tslint/tests/index.spec.ts
Expand Up @@ -12,7 +12,7 @@ const ruleTester = new TSESLint.RuleTester({
* Project is needed to generate the parserServices
* within @typescript-eslint/parser
*/
project: './tests/tsconfig.json',
project: './tests/fixture-project/tsconfig.json',
},
parser: require.resolve('@typescript-eslint/parser'),
});
Expand Down Expand Up @@ -47,6 +47,7 @@ ruleTester.run('tslint/config', rule, {
{
code: 'var foo = true;',
options: tslintRulesConfig,
filename: './tests/fixture-project/1.ts',
},
{
filename: './tests/test-project/file-spec.ts',
Expand All @@ -62,13 +63,15 @@ ruleTester.run('tslint/config', rule, {
{
code: 'throw "should be ok because rule is not loaded";',
options: tslintRulesConfig,
filename: './tests/fixture-project/2.ts',
},
],

invalid: [
{
options: [{ lintFile: './tests/test-project/tslint.json' }],
code: 'throw "err" // no-string-throw',
filename: './tests/fixture-project/3.ts',
errors: [
{
messageId: 'failure',
Expand All @@ -84,6 +87,7 @@ ruleTester.run('tslint/config', rule, {
code: 'var foo = true // semicolon',
options: tslintRulesConfig,
output: 'var foo = true // semicolon',
filename: './tests/fixture-project/4.ts',
errors: [
{
messageId: 'failure',
Expand All @@ -100,6 +104,7 @@ ruleTester.run('tslint/config', rule, {
code: 'var foo = true // fail',
options: tslintRulesDirectoryConfig,
output: 'var foo = true // fail',
filename: './tests/fixture-project/5.ts',
errors: [
{
messageId: 'failure',
Expand Down Expand Up @@ -174,26 +179,30 @@ describe('tslint/error', () => {
});
});

it('should not crash if there is no tslint rules specified', () => {
it('should not crash if there are no tslint rules specified', () => {
const linter = new TSESLint.Linter();
jest.spyOn(console, 'warn').mockImplementation();
linter.defineRule('tslint/config', rule);
linter.defineParser('@typescript-eslint/parser', parser);
expect(() =>
linter.verify('foo;', {
parserOptions: {
project: `${__dirname}/test-project/tsconfig.json`,
},
rules: {
'tslint/config': [2, {}],
linter.verify(
'foo;',
{
parserOptions: {
project: `${__dirname}/test-project/tsconfig.json`,
},
rules: {
'tslint/config': [2, {}],
},
parser: '@typescript-eslint/parser',
},
parser: '@typescript-eslint/parser',
}),
`${__dirname}/test-project/extra.ts`,
),
).not.toThrow();

expect(console.warn).toHaveBeenCalledWith(
expect.stringContaining(
'Tried to lint <input> but found no valid, enabled rules for this file type and file path in the resolved configuration.',
`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.`,
),
);
jest.resetAllMocks();
Expand Down
1 change: 1 addition & 0 deletions packages/eslint-plugin-tslint/tests/test-project/extra.ts
@@ -0,0 +1 @@
foo;
25 changes: 25 additions & 0 deletions packages/eslint-plugin/tests/RuleTester.ts
Expand Up @@ -7,6 +7,8 @@ type RuleTesterConfig = Omit<TSESLint.RuleTesterConfig, 'parser'> & {
parser: typeof parser;
};
class RuleTester extends TSESLint.RuleTester {
private filename: string | undefined = undefined;

// as of eslint 6 you have to provide an absolute path to the parser
// but that's not as clean to type, this saves us trying to manually enforce
// that contributors require.resolve everything
Expand All @@ -15,6 +17,10 @@ class RuleTester extends TSESLint.RuleTester {
...options,
parser: require.resolve(options.parser),
});

if (options.parserOptions && options.parserOptions.project) {
this.filename = path.join(getFixturesRootDir(), 'file.ts');
}
}

// as of eslint 6 you have to provide an absolute path to the parser
Expand All @@ -26,17 +32,36 @@ class RuleTester extends TSESLint.RuleTester {
tests: TSESLint.RunTests<TMessageIds, TOptions>,
): void {
const errorMessage = `Do not set the parser at the test level unless you want to use a parser other than ${parser}`;

if (this.filename) {
tests.valid = tests.valid.map(test => {
if (typeof test === 'string') {
return {
code: test,
filename: this.filename,
};
}
return test;
});
}

tests.valid.forEach(test => {
if (typeof test !== 'string') {
if (test.parser === parser) {
throw new Error(errorMessage);
}
if (!test.filename) {
test.filename = this.filename;
}
}
});
tests.invalid.forEach(test => {
if (test.parser === parser) {
throw new Error(errorMessage);
}
if (!test.filename) {
test.filename = this.filename;
}
});

super.run(name, rule, tests);
Expand Down
Empty file.
Expand Up @@ -199,7 +199,6 @@ import * as Foo from './foo';
declare module './foo' {
const x: Foo.T = 3;
}`,
filename: path.join(rootPath, 'bar.ts'),
errors: [
{
messageId,
Expand Down
2 changes: 1 addition & 1 deletion packages/parser/README.md
Expand Up @@ -48,7 +48,7 @@ The following additional configuration options are available by specifying them

- **`useJSXTextNode`** - default `true`. Please set `false` if you use this parser on ESLint v4. If this is `false`, the parser creates the AST of JSX texts as the legacy style.

- **`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.
- **`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. Note that if this setting is specified, you must only lint files that are included in the projects as defined by the provided `tsconfig.json` files.
uniqueiniquity marked this conversation as resolved.
Show resolved Hide resolved

- **`tsconfigRootDir`** - default `undefined`. This option allows you to provide the root directory for relative tsconfig paths specified in the `project` option above.

Expand Down
4 changes: 2 additions & 2 deletions packages/parser/tests/lib/parser.ts
Expand Up @@ -48,12 +48,12 @@ describe('parser', () => {
jsx: false,
},
// ts-estree specific
filePath: 'test/foo',
filePath: 'tests/fixtures/services/isolated-file.src.ts',
project: 'tsconfig.json',
useJSXTextNode: false,
errorOnUnknownASTType: false,
errorOnTypeScriptSyntacticAndSemanticIssues: false,
tsconfigRootDir: './',
tsconfigRootDir: 'tests/fixtures/services',
extraFileExtensions: ['foo'],
};
parseForESLint(code, config);
Expand Down
37 changes: 12 additions & 25 deletions packages/typescript-estree/src/parser.ts
Expand Up @@ -6,10 +6,7 @@ import { firstDefined } from './node-utils';
import { Extra, TSESTreeOptions, ParserServices } from './parser-options';
import { getFirstSemanticOrSyntacticError } from './semantic-errors';
import { TSESTree } from './ts-estree';
import {
calculateProjectParserOptions,
createProgram,
} from './tsconfig-parser';
import { calculateProjectParserOptions } from './tsconfig-parser';

/**
* This needs to be kept in sync with the top-level README.md in the
Expand Down Expand Up @@ -67,31 +64,22 @@ function resetExtra(): void {
* @returns If found, returns the source file corresponding to the code and the containing program
*/
function getASTFromProject(code: string, options: TSESTreeOptions) {
return firstDefined(
calculateProjectParserOptions(
code,
options.filePath || getFileName(options),
extra,
),
const filePath = options.filePath || getFileName(options);
const astAndProgram = firstDefined(
calculateProjectParserOptions(code, filePath, extra),
currentProgram => {
const ast = currentProgram.getSourceFile(
options.filePath || getFileName(options),
);
const ast = currentProgram.getSourceFile(filePath);
return ast && { ast, program: currentProgram };
},
);
}

/**
* @param code The code of the file being linted
* @param options The config object
* @returns If found, returns the source file corresponding to the code and the containing program
*/
function getASTAndDefaultProject(code: string, options: TSESTreeOptions) {
uniqueiniquity marked this conversation as resolved.
Show resolved Hide resolved
const fileName = options.filePath || getFileName(options);
const program = createProgram(code, fileName, extra);
const ast = program && program.getSourceFile(fileName);
return ast && { ast, program };
if (!astAndProgram) {
throw new Error(
`If "parserOptions.project" has been set for @typescript-eslint/parser, ${filePath} must be included in at least one of the projects provided.`,
);
}

return astAndProgram;
}

/**
Expand Down Expand Up @@ -164,7 +152,6 @@ function getProgramAndAST(
) {
return (
(shouldProvideParserServices && getASTFromProject(code, options)) ||
(shouldProvideParserServices && getASTAndDefaultProject(code, options)) ||
createNewProgram(code)
);
}
Expand Down
10 changes: 10 additions & 0 deletions packages/typescript-estree/src/tsconfig-parser.ts
Expand Up @@ -30,6 +30,16 @@ const watchCallbackTrackingMap = new Map<string, ts.FileWatcherCallback>();

const parsedFilesSeen = new Set<string>();

/**
* Clear tsconfig caches.
* Primarily used for testing.
*/
export function clearCaches() {
knownWatchProgramMap.clear();
watchCallbackTrackingMap.clear();
parsedFilesSeen.clear();
}

/**
* Holds information about the file currently being linted
*/
Expand Down
Empty file.
@@ -0,0 +1 @@
{}
25 changes: 18 additions & 7 deletions packages/typescript-estree/tests/lib/parse.ts
Expand Up @@ -2,6 +2,9 @@ import * as parser from '../../src/parser';
import * as astConverter from '../../src/ast-converter';
import { TSESTreeOptions } from '../../src/parser-options';
import { createSnapshotTestBlock } from '../../tools/test-utils';
import { join } from 'path';

const FIXTURES_DIR = './tests/fixtures/simpleProject';

describe('parse()', () => {
describe('basic functionality', () => {
Expand Down Expand Up @@ -135,6 +138,12 @@ describe('parse()', () => {
tokens: true,
range: true,
loc: true,
filePath: 'tests/fixtures/simpleProject/file.ts',
};
const projectConfig: TSESTreeOptions = {
...baseConfig,
tsconfigRootDir: join(process.cwd(), FIXTURES_DIR),
project: './tsconfig.json',
};

it('should not impact the use of parse()', () => {
Expand Down Expand Up @@ -165,10 +174,10 @@ describe('parse()', () => {
expect(noOptionSet.services.esTreeNodeToTSNodeMap).toBeUndefined();
expect(noOptionSet.services.tsNodeToESTreeNodeMap).toBeUndefined();

const withProjectNoOptionSet = parser.parseAndGenerateServices(code, {
...baseConfig,
project: './tsconfig.json',
});
const withProjectNoOptionSet = parser.parseAndGenerateServices(
code,
projectConfig,
);

expect(withProjectNoOptionSet.services.esTreeNodeToTSNodeMap).toEqual(
expect.any(WeakMap),
Expand All @@ -192,9 +201,8 @@ describe('parse()', () => {
);

const withProjectOptionSetToTrue = parser.parseAndGenerateServices(code, {
...baseConfig,
...projectConfig,
preserveNodeMaps: true,
project: './tsconfig.json',
});

expect(withProjectOptionSetToTrue.services.esTreeNodeToTSNodeMap).toEqual(
Expand All @@ -216,7 +224,10 @@ describe('parse()', () => {

const withProjectOptionSetToFalse = parser.parseAndGenerateServices(
code,
{ ...baseConfig, preserveNodeMaps: false, project: './tsconfig.json' },
{
...projectConfig,
preserveNodeMaps: false,
},
);

expect(
Expand Down