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

Use union types for all common classes of AST nodes #54148

Closed
wants to merge 1 commit into from

Conversation

weswigham
Copy link
Member

Like Node, Expression, TypeNode, and the like.

This allows us to make many more switch statements in our codebase exhaustive (a change not included in this PR, which just swaps over to unions and fixes resulting errors/lints).

This is like the Compiler-Unions perf test, except:

  1. Totally complete, rather than just the compiler folder.
  2. Totally up-to-date (this comment will age well 😂).

Incremental rebuilds are a rebuild-accelerating thing now, so I think this is more palatable nowadays (because a fresh build does still take about 2.7x longer than main today does). Especially since using unions for all these types has tangible style and safety advantages (no more casts everywhere!). Immediately, this found some dead code (which is now gone) and some incorrect types (which are now fixed). In addition to the ergonomic niceties of using unions like this, I do think using them more like this would help us dogfood our own typesystem more in scenarios we consider more "extreme", since we have 363 different node kinds now (and usually growing), so the Node type is what we should consider a "reasonable" but large type.

There are a handful of places where I had to introduce some new casts to work around some bogus complexity errors (opened #54146 to track the issue causing it, and flagged the offending locations), but other than that this actually works pretty well without significant refactoring (other than automatically removing a ton of useless casts). The big question is really just if we, as a team, think the tradeoffs are worth it at this point or not.

@@ -649,7 +639,7 @@ function createBinder(): (file: SourceFile, options: CompilerOptions) => void {
// unless it is a well known Symbol.
function getDeclarationName(node: Declaration): __String | undefined {
if (node.kind === SyntaxKind.ExportAssignment) {
return (node as ExportAssignment).isExportEquals ? InternalSymbolName.ExportEquals : InternalSymbolName.Default;
return (node).isExportEquals ? InternalSymbolName.ExportEquals : InternalSymbolName.Default;
Copy link
Member

Choose a reason for hiding this comment

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

Scrolling though the diff but noticed this leftover parens; not sure if it's also leftover anywhere else.

(Formatter when)

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, it looks like the eslint --fix command left behind a lot of unneeded parenthesis (and some whitespace in other places!), and we have no other lint rule forbidding them.

Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Member

Choose a reason for hiding this comment

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

There are more than a few others in this file (L819, L1208, etc.)

Copy link
Contributor

@JoshuaKGoldberg JoshuaKGoldberg May 5, 2023

Choose a reason for hiding this comment

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

Yup, @typescript-eslint/no-unnecessary-type-assertion follows in our standard practice of not trying to adjust so much for formatting. https://typescript-eslint.io/linting/troubleshooting/formatting

(I realize you all know the differences between the tools - just posting the page for context around why we consider this not a bug in the rule)

Looking forward to TypeScript using dprint 😄

Copy link
Contributor

Choose a reason for hiding this comment

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

Personally, I wouldn't call this a formatting concern though. Extra parenthesis are rarely something that people do on purpose and, in here, the original code required those parenthesis not for formatting purposes. I think it would be reasonable to strip those away while fixing as the original intent is pretty clear here.

@jakebailey
Copy link
Member

Incremental rebuilds are a rebuild-accelerating thing now, so I think this is more palatable nowadays (because a fresh build does still take about 2.7x longer than main today does).

Definitely curious to do a differential profile on this one.

@@ -374,7 +364,7 @@ function getModuleInstanceStateWorker(node: Node, visited: Map<number, ModuleIns
break;
// 4. Export alias declarations pointing at only uninstantiated modules or things uninstantiated modules contain
case SyntaxKind.ExportDeclaration:
const exportDeclaration = node as ExportDeclaration;
const exportDeclaration = node ;
Copy link
Member Author

Choose a reason for hiding this comment

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

See this whitespace? This is almost certainly a bug in the eslint rule's --fix behavior. What's depressing is how many instances aren't caught by any of the whitespace-checking rules we have on, thoguh.

Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Contributor

Choose a reason for hiding this comment

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

#54148 (comment) 🤝 ❤️

Copy link
Member

@rbuckton rbuckton left a comment

Choose a reason for hiding this comment

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

I'm still working through my review, but there are quite a few cases of excess parens and excess whitespace left over after this change. There also appear to be a number of potentially unnecessary type assertions remaining, so it might be worthwhile to look into usages of ForInOrOfStatement, AssertionExpression, BreakOrContinueStatement, AccessorDeclaration, and a few others as well.

break;
case SyntaxKind.VariableDeclaration:
bindVariableDeclarationFlow(node as VariableDeclaration);
bindVariableDeclarationFlow(node);
break;
case SyntaxKind.PropertyAccessExpression:
case SyntaxKind.ElementAccessExpression:
bindAccessExpressionFlow(node as AccessExpression);
Copy link
Member

Choose a reason for hiding this comment

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

Shouldn't this cast be unnecessary?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, I'm surprised it wasn't removed. Maybe because the lint rules uses object equality, and the aliased form of the union isn't === to the non-aliased form of the union?

Copy link
Contributor

@JoshuaKGoldberg JoshuaKGoldberg May 6, 2023

Choose a reason for hiding this comment

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

Yes, that's exactly it:

https://github.com/typescript-eslint/typescript-eslint/blob/09c04fb0d5a5dee0dc266abb2e21aa9a1c489712/packages/eslint-plugin/src/rules/no-unnecessary-type-assertion.ts#L260

We're blocked on making more informed decisions on the existence of a public type assignability API: #9879. In theory we could try to be smart about breaking apart unions, but if we went down that rabbit hole it'd eventually end up with us reimplementing much of TypeScript's native type assignability. We've drawn the line at just === checking.

(We have so many great feature requests on the typescript-eslint side blocked on that issue, it's nice to also add a link to it in TypeScript itself 😄)

break;
case SyntaxKind.ReturnStatement:
case SyntaxKind.ThrowStatement:
bindReturnOrThrow(node as ReturnStatement | ThrowStatement);
bindReturnOrThrow(node);
break;
case SyntaxKind.BreakStatement:
case SyntaxKind.ContinueStatement:
bindBreakOrContinueStatement(node as BreakOrContinueStatement);
Copy link
Member

Choose a reason for hiding this comment

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

Shouldn't this cast be unnecessary?

Copy link
Contributor

Choose a reason for hiding this comment

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

#54148 (comment)

(here and the other comments like this one)

break;
case SyntaxKind.ForStatement:
bindForStatement(node as ForStatement);
bindForStatement(node);
break;
case SyntaxKind.ForInStatement:
case SyntaxKind.ForOfStatement:
bindForInOrForOfStatement(node as ForInOrOfStatement);
Copy link
Member

Choose a reason for hiding this comment

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

Shouldn't this cast be unnecessary?

case SyntaxKind.ParenthesizedExpression:
case SyntaxKind.NonNullExpression:
return isNarrowingExpression((expr as ParenthesizedExpression | NonNullExpression).expression);
return isNarrowingExpression((expr).expression);
Copy link
Member

Choose a reason for hiding this comment

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

Excess parens.

case SyntaxKind.PrefixUnaryExpression:
return (expr as PrefixUnaryExpression).operator === SyntaxKind.ExclamationToken && isNarrowingExpression((expr as PrefixUnaryExpression).operand);
return (expr).operator === SyntaxKind.ExclamationToken && isNarrowingExpression((expr).operand);
Copy link
Member

Choose a reason for hiding this comment

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

Excess parens.

@@ -2170,11 +2169,11 @@ export function createPrinter(printerOptions: PrinterOptions = {}, handlers: Pri
case SyntaxKind.JSDocSatisfiesTag:
return emitJSDocSimpleTypedTag(node as JSDocTypeTag | JSDocReturnTag | JSDocThisTag | JSDocTypeTag | JSDocThrowsTag | JSDocSatisfiesTag);
Copy link
Member

Choose a reason for hiding this comment

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

Is this cast still necessary?

@@ -252,14 +251,14 @@ export function createParenthesizerRules(factory: NodeFactory): ParenthesizerRul
return node.kind;
}

if (node.kind === SyntaxKind.BinaryExpression && (node as BinaryExpression).operatorToken.kind === SyntaxKind.PlusToken) {
if (node.kind === SyntaxKind.BinaryExpression && (node).operatorToken.kind === SyntaxKind.PlusToken) {
Copy link
Member

Choose a reason for hiding this comment

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

Excess parens.

@@ -7570,6 +7583,9 @@ export function createSourceMapSource(fileName: string, text: string, skipTrivia

// Utilities

/** @internal */ export function setOriginalNode<T extends Node>(node: Mutable<T>, original: Node | undefined): T; // TODO: This should probably always require a Mutable<T>, since it mutates `original`
Copy link
Member

Choose a reason for hiding this comment

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

The TODO isn't relevant since original isn't marked readonly on Node.

@@ -7570,6 +7583,9 @@ export function createSourceMapSource(fileName: string, text: string, skipTrivia

// Utilities

/** @internal */ export function setOriginalNode<T extends Node>(node: Mutable<T>, original: Node | undefined): T; // TODO: This should probably always require a Mutable<T>, since it mutates `original`
// eslint-disable-next-line @typescript-eslint/unified-signatures
export function setOriginalNode<T extends Node>(node: T, original: Node | undefined): T; // inference is worse if the signatures are combined
Copy link
Member

Choose a reason for hiding this comment

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

Does this comment show up in the public API? It would be somewhat confusing if so, since the other overload isn't public.

@@ -10124,7 +10126,8 @@ namespace IncrementalParser {
_children: Node[] | undefined;
}

export interface IncrementalNode extends Node, IncrementalElement {
export type IncrementalNode = Node & IncrementalNodeBase;
export interface IncrementalNodeBase extends NodeBase, IncrementalElement {
Copy link
Member

Choose a reason for hiding this comment

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

Not for this PR, but this interface smells like megamorphism and we should probably find a different way to mark incrementally parsed nodes. I'm not sure how many other cases of "convert this interface to an intersection to add an extra property" there are, but this PR is surfacing polymorphic assignment. We should probably add comments to these cases so we can identify them later.

Separately, we might want to consider a ts-eslint rule that warns on any intersection with Node or its subtypes that introduces net-new properties.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, I thought the same thing when making the change. Luckily it wasn't that common.

@gabritto
Copy link
Member

gabritto commented May 5, 2023

because a fresh build does still take about 2.7x longer than main today does

What's the impact on an editor scenario? Say, editing checker.ts. It's already pretty bad today (at least for me), and though I don't care about a fresh build being 3x as slow, I do care about editor perf.

@jakebailey
Copy link
Member

jakebailey commented May 5, 2023

A quick glance at a profile for this shows a bunch of suspicious new calls for getResolvedSignature which spend a lot of time in inferTypeArguments (a full second each time it happens). Seems like maybe there's one problematic function?

The 2.7x slowdown is definitely there, though.

pprof-time-92423.pb.gz

pprof-time-92885.pb.gz

@jakebailey
Copy link
Member

jakebailey commented May 5, 2023

I did a quick debug print to show which of these getResolvedSignatures took more than 500ms, and they are these expressions:

new (TokenConstructor || (TokenConstructor = objectAllocator.getTokenConstructor()))(kind, /*pos*/ -1, /*end*/ -1)
createToken(value)
new (TokenConstructor || (TokenConstructor = objectAllocator.getTokenConstructor()))(kind, -1, -1)
parseOptionalToken(t)

Ouch:

image

image

Also, editing in this branch unfortunately feels quite slow. Especially if you're me and have eslint running :(

@@ -7570,6 +7583,9 @@ export function createSourceMapSource(fileName: string, text: string, skipTrivia

// Utilities

/** @internal */ export function setOriginalNode<T extends Node>(node: Mutable<T>, original: Node | undefined): T; // TODO: This should probably always require a Mutable<T>, since it mutates `original`
// eslint-disable-next-line @typescript-eslint/unified-signatures
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a bugfix or feature request you'd want to file on typescript-eslint to make unified-signatures not complain on this case?

@weswigham weswigham closed this Aug 13, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Author: Team Experiment A fork with an experimental idea which might not make it into master For Uncommitted Bug PR for untriaged, rejected, closed or missing bug
Projects
Status: Done
Development

Successfully merging this pull request may close these issues.

7 participants