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

Convert repo to TypeScript; Auto-generate types for defs #300

Merged
merged 159 commits into from
Jan 9, 2019

Conversation

brieb
Copy link
Contributor

@brieb brieb commented Nov 19, 2018

Following up from our twitter thread: https://twitter.com/benjamn/status/1063979819149475841

Started down the path of making d.ts files, but in combing through the code to make sure the types were correct, I increasingly felt like I'd have more confidence if we just converted the code to TS. Sounded like you were open to this idea from your twitter comment.

Goals:

  • Convert all files from JS to TS
  • Emit TypeScript declarations
  • Auto-generate types for types/builders/visitor, as exported from main
  • Preserve all runtime behavior / only modify the code in the type space
  • Backwards compatibility
    • Keep folder structure for emitted JS, to not change import paths for users
    • Keep module.exports = by using TypeScript's export =, so default export behavior doesn't change

TODO:

  • The types are pretty loose (lots of any), but are at least correct. We'll want to narrow these types over time.

The results are pretty awesome!

image

Reviewers: @benjamn

Don't force node type field to be the type name as a string literal.
Only buildable types have a type field set to their type, as per
https://github.com/brieb/ast-types/blob/94ff83cc3f9e971aecac0fdfa3c931f482e13a5a/lib/types.ts#L533
So, the previous types weren't true to what we'd encounter at runtime.
This allows us to use ASTNode as a type and narrow it based on the
`type` field. i.e.

```
function exampleFn(node: ASTNode) {
  if (node.type === "Identifier") {
    // node is narrowed to `Identifier` here
  }
}
```

This pattern can also be seen in @babel/types
https://gist.github.com/brieb/e312c8f2565c4b2cde5996e1f648780e#file-index-d-ts-L44
When `string` is introduced to a union type, we are no longer able
to narrow types because the type widens to `string` rather than a string literal
union.

Here is an example that demonstrates this: http://bit.ly/2QyiplJ

Usage of the unioned node's type is an approach also used by `@babel/types`.
https://gist.github.com/brieb/e312c8f2565c4b2cde5996e1f648780e#file-index-d-ts-L41
My guess is that this was added to address this situation.
@@ -264,20 +268,23 @@ describe("whole-program validation", function() {
this.timeout(20000);

// TODO(brieb): do we need these?
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this is the only remaining TODO(brieb) in this PR. Before, we ran validateECMAScript on this repo's source files ("main.js", "lib/shared.js", etc). How should we address these test cases, now that the source code is .ts? Should we run it on the emitted files, add different more targeted test cases as new fixtures, remove them, something else?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be useful to run namedTypes.Program.assert(ast, true) on some TypeScript ASTs, to assert that everything in the AST matches the expected types. Basically the same as shared.validateECMAScript, except it would only use the Babel TypeScript parser, not Esprima.

Actually, scratch that! It looks like we already do a deep assertion for every file we process in tests/typescript.ts here, which runs over the whole https://github.com/Microsoft/TypeScript/tree/master/src/compiler codebase.

In light of that, I'm pretty sure the commented-out tests are redundant, and we can just remove them.

So, we can use type narrowing for fields of type `*Kind`.
For example,
```
function exampleFn(node: ASTNode) {
  if (node.type === "CatchClause") {
    if (node.param) {
      if (node.param.type === "Identifier") {
        // `node.param` is properly narrowed to `Identifier` here
        console.log(node.param.name);
      }
    }
  }
}
```
@brieb
Copy link
Contributor Author

brieb commented Jan 8, 2019

Cool - I think we're good to go as long as you think this change #300 (comment) / 2483cd2 makes sense!

@benjamn
Copy link
Owner

benjamn commented Jan 9, 2019

Makes sense to me!

So… if ASTNode is a union of all buildable node interface types, and each *Kind type is also a union of (some) buildable node interface types, then I guess ASTNode is equivalent to the union of all *Kind types? I think the way you're currently defining ASTNode is simpler/better, but it seems like it could also be written

export type ASTNode = K.PrintableKind | K.NodeKind | K.FileKind | ...

If I'm understanding correctly, it's kinda neat how nodes and kinds are related via ASTNode.

I noticed one small oddity: SourceLocation currently has type: "SourceLocation", even though parsed { start, end } objects generally don't have a type field. This could be misleading if you wrote code like

if (node.type === "SourceLocation") {
  doSomething(node.start, node.end);
}

since no node would ever match that case.

However, that's just because I made SourceLocation buildable here. Honestly I don't think we need a builder function for SourceLocation nodes, or Position nodes, for that matter. I'll push a commit to fix that.

The `SourceLocation` definition currently has `type:"SourceLocation"`,
even though `{ start, end, source }` location objects don't have a `type`
field when they're parsed. Same for the `Position` definition. This could
be misleading if you wrote code like

  if (node.type === "SourceLocation") {
    doSomething(node.start, node.end);
  }

since no node would ever match that case.

Although I guess we could introduce a buildable node type that doesn't
have a `type` field, I think it's simpler to remove the builder functions
for SourceLocation and Position.
.field("start", def("Position"))
.field("end", def("Position"))
.field("source", or(String, null), defaults["null"]);

def("Position")
.build("line", "column")
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's so great to see the immediate effects of a commit in the generated code. 💚

@benjamn benjamn merged commit 155a4ac into benjamn:master Jan 9, 2019
@brieb
Copy link
Contributor Author

brieb commented Jan 9, 2019

Merged! Woo! 🎉


Yes, it should be the case that the union of all kinds resolves to the same union as ASTNode with one exception - some of the XML* types didn't have buildable subtypes, so I just had it fall back to the supertype's node type.
https://github.com/brieb/ast-types/blob/fd95109b3e8e4b3e4719a957b931a5805a2983a4/script/gen-types.ts#L40-L49
https://github.com/brieb/ast-types/blob/fd95109b3e8e4b3e4719a957b931a5805a2983a4/gen/kinds.ts#L92-L110

Here is the full list of supertypes without buildable subtypes (cases where buildableSubtypes.length === 0)

XMLDefaultDeclaration
XMLAnyName
XMLQualifiedIdentifier
XMLFunctionQualifiedIdentifier
XMLAttributeSelector
XMLFilterExpression
XML
XMLElement
XMLList
XMLEscape
XMLText
XMLStartTag
XMLEndTag
XMLPointTag
XMLName
XMLAttribute
XMLCdata
XMLComment
XMLProcessingInstruction

@brieb brieb deleted the typescript branch January 9, 2019 00:32
@benjamn
Copy link
Owner

benjamn commented Jan 9, 2019

Oh geez, we should probably stop loading def/e4x.ts by default. E4X was a version of JavaScript before ES5 that was abandoned by TC39. The big new feature (never realized) was XML literals, so the original Mozilla Parser API (which inspired the Esprima parser) still included those XML* types, which is the only reason I included them in this project.

@benjamn
Copy link
Owner

benjamn commented Jan 9, 2019

Ok, I've published version 0.12.0 (with this PR included) to npm using the next tag. That should make ast-types@0.12.0 easier to consume in Recast and elsewhere. Happy to promote it to latest whenever it seems ready.

@brieb
Copy link
Contributor Author

brieb commented Jan 9, 2019

Awesome! Just updated benjamn/recast#554 to use that version.

benjamn added a commit that referenced this pull request Jan 10, 2019
#300 (comment)

The def/e4x.js definitions were inherited from the Mozilla Parser API, and
represent a language feature (XML literals) that has long been abandoned.

The Mozilla extensions (let statements, expression function bodies, &c.)
have likewise become ancient history, not even still supported by Firefox.
benjamn added a commit that referenced this pull request Jan 10, 2019
#300 (comment)

The def/e4x.js definitions were inherited from the Mozilla Parser API, and
represent a language feature (XML literals) that has long been abandoned.

The Mozilla extensions (let statements, expression function bodies, &c.)
have likewise become ancient history, not even still supported by Firefox.
benjamn added a commit that referenced this pull request Jan 10, 2019
#300 (comment)

The def/e4x.js definitions were inherited from the Mozilla Parser API, and
represent a language feature (XML literals) that has long been abandoned.

The Mozilla extensions (let statements, expression function bodies, &c.)
have likewise become ancient history, not even still supported by Firefox.
bnjmnt4n added a commit to bnjmnt4n/ast-types that referenced this pull request Feb 6, 2019
Since benjamn#300 removed the legacy Mozilla and E4X AST definitions, the
legacy `..` operator should be removed as well.

Closes benjamn#238.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants