Skip to content

Commit

Permalink
feat(typescript-estree): always return parserServices (#716)
Browse files Browse the repository at this point in the history
  • Loading branch information
bradzacher committed May 21, 2020
1 parent 9a96e18 commit 5b23443
Show file tree
Hide file tree
Showing 15 changed files with 211 additions and 191 deletions.
2 changes: 1 addition & 1 deletion packages/eslint-plugin/README.md
Expand Up @@ -139,7 +139,7 @@ Pro Tip: For larger codebases you may want to consider splitting our linting int
| [`@typescript-eslint/no-unsafe-call`](./docs/rules/no-unsafe-call.md) | Disallows calling an any type value | | | :thought_balloon: |
| [`@typescript-eslint/no-unsafe-member-access`](./docs/rules/no-unsafe-member-access.md) | Disallows member access on any typed variables | | | :thought_balloon: |
| [`@typescript-eslint/no-unsafe-return`](./docs/rules/no-unsafe-return.md) | Disallows returning any from a function | | | :thought_balloon: |
| [`@typescript-eslint/no-unused-vars-experimental`](./docs/rules/no-unused-vars-experimental.md) | Disallow unused variables and arguments | | | :thought_balloon: |
| [`@typescript-eslint/no-unused-vars-experimental`](./docs/rules/no-unused-vars-experimental.md) | Disallow unused variables and arguments | | | |
| [`@typescript-eslint/no-var-requires`](./docs/rules/no-var-requires.md) | Disallows the use of require statements except in import statements | :heavy_check_mark: | | |
| [`@typescript-eslint/prefer-as-const`](./docs/rules/prefer-as-const.md) | Prefer usage of `as const` over literal type | | :wrench: | |
| [`@typescript-eslint/prefer-for-of`](./docs/rules/prefer-for-of.md) | Prefer a ‘for-of’ loop over a standard ‘for’ loop if the index is only used to access the array being iterated | | | |
Expand Down
Expand Up @@ -29,7 +29,6 @@ export default util.createRule<Options, MessageIds>({
description: 'Disallow unused variables and arguments',
category: 'Best Practices',
recommended: false,
requiresTypeChecking: true,
},
schema: [
{
Expand Down Expand Up @@ -68,7 +67,7 @@ export default util.createRule<Options, MessageIds>({
},
],
create(context, [userOptions]) {
const parserServices = util.getParserServices(context);
const parserServices = util.getParserServices(context, true);
const tsProgram = parserServices.program;
const afterAllDiagnosticsCallbacks: (() => void)[] = [];

Expand Down Expand Up @@ -191,7 +190,7 @@ export default util.createRule<Options, MessageIds>({
parent: ts.ParameterDeclaration,
): void {
const name = identifier.getText();
// regardless of if the paramter is ignored, track that it had a diagnostic fired on it
// regardless of if the parameter is ignored, track that it had a diagnostic fired on it
unusedParameters.add(identifier);

/*
Expand Down
7 changes: 6 additions & 1 deletion packages/eslint-plugin/tests/docs.test.ts
Expand Up @@ -70,6 +70,10 @@ describe('Validating rule docs', () => {
});

describe('Validating rule metadata', () => {
function requiresFullTypeInformation(content: string): boolean {
return /getParserServices(\(\s*[^,\s)]+)\s*(,\s*false\s*)?\)/.test(content);
}

for (const [ruleName, rule] of rulesData) {
describe(`${ruleName}`, () => {
it('`name` field in rule must match the filename', () => {
Expand All @@ -85,9 +89,10 @@ describe('Validating rule metadata', () => {
// not perfect but should be good enough
const ruleFileContents = fs.readFileSync(
path.resolve(__dirname, `../src/rules/${ruleName}.ts`),
'utf-8',
);

expect(ruleFileContents.includes('getParserServices')).toEqual(
expect(requiresFullTypeInformation(ruleFileContents)).toEqual(
rule.meta.docs?.requiresTypeChecking ?? false,
);
});
Expand Down
Expand Up @@ -14,8 +14,6 @@ const ruleTester = new RuleTester({
parserOptions: {
ecmaVersion: 2018,
sourceType: 'module',
project: './tsconfig.json',
tsconfigRootDir: rootDir,
},
parser: '@typescript-eslint/parser',
});
Expand Down
4 changes: 2 additions & 2 deletions packages/eslint-plugin/tests/util/isUnsafeAssignment.test.ts
Expand Up @@ -16,8 +16,8 @@ describe('isUnsafeAssignment', () => {
filePath: path.join(rootDir, 'file.ts'),
tsconfigRootDir: rootDir,
});
const checker = services.program!.getTypeChecker();
const esTreeNodeToTSNodeMap = services.esTreeNodeToTSNodeMap!;
const checker = services.program.getTypeChecker();
const esTreeNodeToTSNodeMap = services.esTreeNodeToTSNodeMap;

const declaration = ast.body[0] as TSESTree.VariableDeclaration;
const declarator = declaration.declarations[0];
Expand Down
31 changes: 19 additions & 12 deletions packages/experimental-utils/src/eslint-utils/getParserServices.ts
Expand Up @@ -4,31 +4,38 @@ import { ParserServices } from '../ts-estree';
const ERROR_MESSAGE =
'You have used a rule which requires parserServices to be generated. You must therefore provide a value for the "parserOptions.project" property for @typescript-eslint/parser.';

type RequiredParserServices = {
[k in keyof ParserServices]: Exclude<ParserServices[k], undefined>;
};

/**
* Try to retrieve typescript parser service from context
*/
function getParserServices<
TMessageIds extends string,
TOptions extends unknown[]
TOptions extends readonly unknown[]
>(
context: TSESLint.RuleContext<TMessageIds, TOptions>,
): RequiredParserServices {
allowWithoutFullTypeInformation = false,
): ParserServices {
// backwards compatibility check
// old versions of the parser would not return any parserServices unless parserOptions.project was set
if (
!context.parserServices ||
!context.parserServices.program ||
!context.parserServices.esTreeNodeToTSNodeMap
!context.parserServices.esTreeNodeToTSNodeMap ||
!context.parserServices.tsNodeToESTreeNodeMap
) {
/**
* The user needs to have configured "project" in their parserOptions
* for @typescript-eslint/parser
*/
throw new Error(ERROR_MESSAGE);
}
return context.parserServices as RequiredParserServices;

const hasFullTypeInformation =
context.parserServices.hasFullTypeInformation ??
/* backwards compatible */ true;

// if a rule requires full type information, then hard fail if it doesn't exist
// this forces the user to supply parserOptions.project
if (!hasFullTypeInformation && !allowWithoutFullTypeInformation) {
throw new Error(ERROR_MESSAGE);
}

return context.parserServices;
}

export { getParserServices };
4 changes: 2 additions & 2 deletions packages/typescript-estree/src/ast-converter.ts
Expand Up @@ -10,7 +10,7 @@ export function astConverter(
ast: SourceFile,
extra: Extra,
shouldPreserveNodeMaps: boolean,
): { estree: TSESTree.Program; astMaps: ASTMaps | undefined } {
): { estree: TSESTree.Program; astMaps: ASTMaps } {
/**
* The TypeScript compiler produced fundamental parse errors when parsing the
* source.
Expand Down Expand Up @@ -63,7 +63,7 @@ export function astConverter(
estree.comments = convertComments(ast, extra.code);
}

const astMaps = shouldPreserveNodeMaps ? instance.getASTMaps() : undefined;
const astMaps = instance.getASTMaps();

return { estree, astMaps };
}
Expand Up @@ -93,7 +93,6 @@ function createProjectProgram(
errorLines.push(
'The file must be included in at least one of the projects provided.',
);
hasMatchedAnError = true;
}

throw new Error(errorLines.join('\n'));
Expand Down
5 changes: 4 additions & 1 deletion packages/typescript-estree/src/create-program/shared.ts
Expand Up @@ -4,7 +4,7 @@ import { Extra } from '../parser-options';

interface ASTAndProgram {
ast: ts.SourceFile;
program: ts.Program | undefined;
program: ts.Program;
}

/**
Expand All @@ -16,6 +16,9 @@ const DEFAULT_COMPILER_OPTIONS: ts.CompilerOptions = {
checkJs: true,
noEmit: true,
// extendedDiagnostics: true,
/**
* Flags required to make no-unused-vars work
*/
noUnusedLocals: true,
noUnusedParameters: true,
};
Expand Down
9 changes: 4 additions & 5 deletions packages/typescript-estree/src/parser-options.ts
Expand Up @@ -184,9 +184,8 @@ export interface ParserWeakMapESTreeToTSNode<
}

export interface ParserServices {
program: Program | undefined;
esTreeNodeToTSNodeMap: ParserWeakMapESTreeToTSNode | undefined;
tsNodeToESTreeNodeMap:
| ParserWeakMap<TSNode | TSToken, TSESTree.Node>
| undefined;
program: Program;
esTreeNodeToTSNodeMap: ParserWeakMapESTreeToTSNode;
tsNodeToESTreeNodeMap: ParserWeakMap<TSNode | TSToken, TSESTree.Node>;
hasFullTypeInformation: boolean;
}
48 changes: 13 additions & 35 deletions packages/typescript-estree/src/parser.ts
Expand Up @@ -12,7 +12,7 @@ import { createSourceFile } from './create-program/createSourceFile';
import { Extra, TSESTreeOptions, ParserServices } from './parser-options';
import { getFirstSemanticOrSyntacticError } from './semantic-or-syntactic-errors';
import { TSESTree } from './ts-estree';
import { ensureAbsolutePath } from './create-program/shared';
import { ASTAndProgram, ensureAbsolutePath } from './create-program/shared';

const log = debug('typescript-eslint:typescript-estree:parser');

Expand Down Expand Up @@ -48,11 +48,6 @@ function enforceString(code: unknown): string {
return code;
}

interface ASTAndProgram {
ast: ts.SourceFile;
program: ts.Program | undefined;
}

/**
* @param code The code of the file being linted
* @param shouldProvideParserServices True if the program should be attempted to be calculated from provided tsconfig files
Expand All @@ -63,7 +58,7 @@ function getProgramAndAST(
code: string,
shouldProvideParserServices: boolean,
shouldCreateDefaultProgram: boolean,
): ASTAndProgram | undefined {
): ASTAndProgram {
return (
(shouldProvideParserServices &&
createProjectProgram(code, shouldCreateDefaultProgram, extra)) ||
Expand Down Expand Up @@ -103,7 +98,7 @@ function resetExtra(): void {
jsx: false,
loc: false,
log: console.log, // eslint-disable-line no-console
preserveNodeMaps: undefined,
preserveNodeMaps: true,
projects: [],
range: false,
strict: false,
Expand Down Expand Up @@ -236,7 +231,7 @@ function applyParserOptionsToExtra(options: TSESTreeOptions): void {
}

/**
* Get the file extension
* Get the file path
*/
if (typeof options.filePath === 'string' && options.filePath !== '<input>') {
extra.filePath = options.filePath;
Expand Down Expand Up @@ -298,14 +293,9 @@ function applyParserOptionsToExtra(options: TSESTreeOptions): void {
/**
* Allow the user to enable or disable the preservation of the AST node maps
* during the conversion process.
*
* NOTE: For backwards compatibility we also preserve node maps in the case where `project` is set,
* and `preserveNodeMaps` is not explicitly set to anything.
*/
extra.preserveNodeMaps =
typeof options.preserveNodeMaps === 'boolean' && options.preserveNodeMaps;
if (options.preserveNodeMaps === undefined && extra.projects.length > 0) {
extra.preserveNodeMaps = true;
if (typeof options.preserveNodeMaps === 'boolean') {
extra.preserveNodeMaps = options.preserveNodeMaps;
}

extra.createDefaultProgram =
Expand Down Expand Up @@ -449,20 +439,13 @@ function parseAndGenerateServices<T extends TSESTreeOptions = TSESTreeOptions>(
extra.createDefaultProgram,
)!;

/**
* Determine if two-way maps of converted AST nodes should be preserved
* during the conversion process
*/
const shouldPreserveNodeMaps =
extra.preserveNodeMaps !== undefined
? extra.preserveNodeMaps
: shouldProvideParserServices;

/**
* Convert the TypeScript AST to an ESTree-compatible one, and optionally preserve
* mappings between converted and original AST nodes
*/
const { estree, astMaps } = astConverter(ast, extra, shouldPreserveNodeMaps);
const preserveNodeMaps =
typeof extra.preserveNodeMaps === 'boolean' ? extra.preserveNodeMaps : true;
const { estree, astMaps } = astConverter(ast, extra, preserveNodeMaps);

/**
* Even if TypeScript parsed the source code ok, and we had no problems converting the AST,
Expand All @@ -481,15 +464,10 @@ function parseAndGenerateServices<T extends TSESTreeOptions = TSESTreeOptions>(
return {
ast: estree as AST<T>,
services: {
program: shouldProvideParserServices ? program : undefined,
esTreeNodeToTSNodeMap:
shouldPreserveNodeMaps && astMaps
? astMaps.esTreeNodeToTSNodeMap
: undefined,
tsNodeToESTreeNodeMap:
shouldPreserveNodeMaps && astMaps
? astMaps.tsNodeToESTreeNodeMap
: undefined,
hasFullTypeInformation: shouldProvideParserServices,
program,
esTreeNodeToTSNodeMap: astMaps.esTreeNodeToTSNodeMap,
tsNodeToESTreeNodeMap: astMaps.tsNodeToESTreeNodeMap,
},
};
}
Expand Down

0 comments on commit 5b23443

Please sign in to comment.