Skip to content

Commit

Permalink
feat(typescript-estree): add parseWithNodeMaps API (#2760)
Browse files Browse the repository at this point in the history
Ref: prettier/prettier#9636

This allows consumers to reach into the underlying TS AST in cases where our AST doesn't quite solve the use case.

Motivating example:

We don't (currently) error for code unless TS itself throws an error.
TS is _very_ permissive, but that leads to some weird (and invalid) code we don't error for, and don't include in the AST.
For example - this is _syntactically_ valid in the TS parser, but they emit a _semantic_ error:
```ts
@decorator
enum Foo {
  A = 1
}
```
As it's not a valid place for a decorator - we do not emit its information in the ESTree AST, but as TS treats this as a semantic error - it is parse-time valid.
This creates weird states for consumers wherein they cannot see a decorator exists there, which can cause them to ignore it entirely.
For linting - this isn't a problem. But for something like prettier - this means that they'll delete the decorator at format time!
This is very bad.

Until we implement a solution for #1852 - this will allow consumers to bridge the gap themselves.
  • Loading branch information
bradzacher committed Nov 12, 2020
1 parent 535db3b commit 9441d50
Show file tree
Hide file tree
Showing 5 changed files with 684 additions and 585 deletions.
49 changes: 47 additions & 2 deletions packages/typescript-estree/README.md
Expand Up @@ -220,6 +220,18 @@ interface ParseAndGenerateServicesOptions extends ParseOptions {
createDefaultProgram?: boolean;
}

interface ParserServices {
program: ts.Program;
esTreeNodeToTSNodeMap: WeakMap<TSESTree.Node, ts.Node | ts.Token>;
tsNodeToESTreeNodeMap: WeakMap<ts.Node | ts.Token, TSESTree.Node>;
hasFullTypeInformation: boolean;
}

interface ParseAndGenerateServicesResult<T extends TSESTreeOptions> {
ast: TSESTree.Program;
services: ParserServices;
}

const PARSE_AND_GENERATE_SERVICES_DEFAULT_OPTIONS: ParseOptions = {
...PARSE_DEFAULT_OPTIONS,
errorOnTypeScriptSyntacticAndSemanticIssues: false,
Expand All @@ -233,7 +245,7 @@ const PARSE_AND_GENERATE_SERVICES_DEFAULT_OPTIONS: ParseOptions = {
declare function parseAndGenerateServices(
code: string,
options: ParseOptions = PARSE_DEFAULT_OPTIONS,
): TSESTree.Program;
): ParseAndGenerateServicesResult;
```

Example usage:
Expand All @@ -242,14 +254,47 @@ Example usage:
import { parseAndGenerateServices } from '@typescript-eslint/typescript-estree';

const code = `const hello: string = 'world';`;
const ast = parseAndGenerateServices(code, {
const { ast, services } = parseAndGenerateServices(code, {
filePath: '/some/path/to/file/foo.ts',
loc: true,
project: './tsconfig.json',
range: true,
});
```

#### `parseWithNodeMaps(code, options)`

Parses the given string of code with the options provided and returns both the ESTree-compatible AST as well as the node maps.
This allows you to work with both ASTs without the overhead of types that may come with `parseAndGenerateServices`.

```ts
interface ParseWithNodeMapsResult<T extends TSESTreeOptions> {
ast: TSESTree.Program;
esTreeNodeToTSNodeMap: ParserServices['esTreeNodeToTSNodeMap'];
tsNodeToESTreeNodeMap: ParserServices['tsNodeToESTreeNodeMap'];
}

declare function parseWithNodeMaps(
code: string,
options: ParseOptions = PARSE_DEFAULT_OPTIONS,
): ParseWithNodeMapsResult;
```

Example usage:

```js
import { parseWithNodeMaps } from '@typescript-eslint/typescript-estree';

const code = `const hello: string = 'world';`;
const { ast, esTreeNodeToTSNodeMap, tsNodeToESTreeNodeMap } = parseWithNodeMaps(
code,
{
loc: true,
range: true,
},
);
```

### `TSESTree`, `AST_NODE_TYPES` and `AST_TOKEN_TYPES`

Types for the AST produced by the parse functions.
Expand Down
7 changes: 1 addition & 6 deletions packages/typescript-estree/src/index.ts
@@ -1,9 +1,4 @@
export {
AST,
parse,
parseAndGenerateServices,
ParseAndGenerateServicesResult,
} from './parser';
export * from './parser';
export { ParserServices, TSESTreeOptions } from './parser-options';
export { simpleTraverse } from './simple-traverse';
export * from './ts-estree';
Expand Down
31 changes: 28 additions & 3 deletions packages/typescript-estree/src/parser.ts
Expand Up @@ -321,11 +321,24 @@ interface ParseAndGenerateServicesResult<T extends TSESTreeOptions> {
ast: AST<T>;
services: ParserServices;
}
interface ParseWithNodeMapsResult<T extends TSESTreeOptions> {
ast: AST<T>;
esTreeNodeToTSNodeMap: ParserServices['esTreeNodeToTSNodeMap'];
tsNodeToESTreeNodeMap: ParserServices['tsNodeToESTreeNodeMap'];
}

function parse<T extends TSESTreeOptions = TSESTreeOptions>(
code: string,
options?: T,
): AST<T> {
const { ast } = parseWithNodeMaps(code, options);
return ast;
}

function parseWithNodeMaps<T extends TSESTreeOptions = TSESTreeOptions>(
code: string,
options?: T,
): ParseWithNodeMapsResult<T> {
/**
* Reset the parse configuration
*/
Expand Down Expand Up @@ -367,8 +380,13 @@ function parse<T extends TSESTreeOptions = TSESTreeOptions>(
/**
* Convert the TypeScript AST to an ESTree-compatible one
*/
const { estree } = astConverter(ast, extra, false);
return estree as AST<T>;
const { estree, astMaps } = astConverter(ast, extra, false);

return {
ast: estree as AST<T>,
esTreeNodeToTSNodeMap: astMaps.esTreeNodeToTSNodeMap,
tsNodeToESTreeNodeMap: astMaps.tsNodeToESTreeNodeMap,
};
}

function parseAndGenerateServices<T extends TSESTreeOptions = TSESTreeOptions>(
Expand Down Expand Up @@ -450,4 +468,11 @@ function parseAndGenerateServices<T extends TSESTreeOptions = TSESTreeOptions>(
};
}

export { AST, parse, parseAndGenerateServices, ParseAndGenerateServicesResult };
export {
AST,
parse,
parseAndGenerateServices,
parseWithNodeMaps,
ParseAndGenerateServicesResult,
ParseWithNodeMapsResult,
};

0 comments on commit 9441d50

Please sign in to comment.