-
Notifications
You must be signed in to change notification settings - Fork 12.5k
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
Conversation
@@ -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; |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
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.)
There was a problem hiding this comment.
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 😄
There was a problem hiding this comment.
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.
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 ; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@JoshuaKGoldberg my hero
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
#54148 (comment) 🤝 ❤️
There was a problem hiding this 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); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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:
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); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(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); |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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` |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
What's the impact on an editor scenario? Say, editing |
A quick glance at a profile for this shows a bunch of suspicious new calls for The 2.7x slowdown is definitely there, though. |
I did a quick debug print to show which of these
Ouch: 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 |
There was a problem hiding this comment.
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?
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:compiler
folder.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 theNode
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.