-
Notifications
You must be signed in to change notification settings - Fork 15.2k
Add AST helpers used by api-extractor #64439
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| import { ModifierFlags } from "#enums/modifierFlags"; | ||
| import { SyntaxKind } from "#enums/syntaxKind"; | ||
|
|
||
| export function modifierToFlag(kind: SyntaxKind): ModifierFlags { | ||
| switch (kind) { | ||
| case SyntaxKind.StaticKeyword: | ||
| return ModifierFlags.Static; | ||
| case SyntaxKind.PublicKeyword: | ||
| return ModifierFlags.Public; | ||
| case SyntaxKind.ProtectedKeyword: | ||
| return ModifierFlags.Protected; | ||
| case SyntaxKind.PrivateKeyword: | ||
| return ModifierFlags.Private; | ||
| case SyntaxKind.AbstractKeyword: | ||
| return ModifierFlags.Abstract; | ||
| case SyntaxKind.AccessorKeyword: | ||
| return ModifierFlags.Accessor; | ||
| case SyntaxKind.ExportKeyword: | ||
| return ModifierFlags.Export; | ||
| case SyntaxKind.DeclareKeyword: | ||
| return ModifierFlags.Ambient; | ||
| case SyntaxKind.ConstKeyword: | ||
| return ModifierFlags.Const; | ||
| case SyntaxKind.DefaultKeyword: | ||
| return ModifierFlags.Default; | ||
| case SyntaxKind.AsyncKeyword: | ||
| return ModifierFlags.Async; | ||
| case SyntaxKind.ReadonlyKeyword: | ||
| return ModifierFlags.Readonly; | ||
| case SyntaxKind.OverrideKeyword: | ||
| return ModifierFlags.Override; | ||
| case SyntaxKind.InKeyword: | ||
| return ModifierFlags.In; | ||
| case SyntaxKind.OutKeyword: | ||
| return ModifierFlags.Out; | ||
| case SyntaxKind.Decorator: | ||
| return ModifierFlags.Decorator; | ||
| default: | ||
| return ModifierFlags.None; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,13 +1,38 @@ | ||
| import { CharacterCodes } from "#enums/characterCodes"; | ||
| import { JSDeclarationKind } from "#enums/jsDeclarationKind"; | ||
| import { ModifierFlags } from "#enums/modifierFlags"; | ||
| import { NodeFlags } from "#enums/nodeFlags"; | ||
| import { OuterExpressionKinds } from "#enums/outerExpressionKinds"; | ||
| import { SyntaxKind } from "#enums/syntaxKind"; | ||
| import type { | ||
| __String, | ||
| AccessExpression, | ||
| BinaryExpression, | ||
| BindingElement, | ||
| CallExpression, | ||
| Declaration, | ||
| DeclarationName, | ||
| ElementAccessExpression, | ||
| ExportAssignment, | ||
| Expression, | ||
| HasExpression, | ||
| HasInitializer, | ||
| ModifierLike, | ||
| ObjectAssignmentInitializer, | ||
| PropertyAccessExpression, | ||
| PropertyAssignment, | ||
| SourceFile, | ||
| VariableDeclaration, | ||
| } from "./ast.ts"; | ||
| import type { Node } from "./ast.ts"; | ||
| import { | ||
| isAccessExpression, | ||
| isIdentifier, | ||
| isNumericLiteral, | ||
| isStringLiteralLikeNode, | ||
| } from "./is.generated.ts"; | ||
| import { skipOuterExpressions } from "./is.ts"; | ||
| import { modifierToFlag } from "./modifiers.ts"; | ||
|
|
||
| let syntaxKindNames: Map<number, string> | undefined; | ||
| function getSyntaxKindNames(): Map<number, string> { | ||
|
|
@@ -87,6 +112,231 @@ export function hasObjectAssignmentInitializer(node: Node): node is ObjectAssign | |
| return "objectAssignmentInitializer" in node; | ||
| } | ||
|
|
||
| export function isExternalModule(file: SourceFile): boolean { | ||
| return file.externalModuleIndicator !== undefined; | ||
| } | ||
|
|
||
| function getRootDeclaration(node: Node): Node { | ||
| while (node.kind === SyntaxKind.BindingElement) { | ||
| node = node.parent.parent; | ||
| } | ||
| return node; | ||
| } | ||
|
|
||
| function getModifierFlags(node: Node): ModifierFlags { | ||
| const modifiers = "modifiers" in node | ||
| ? (node as Node & { readonly modifiers?: readonly ModifierLike[]; }).modifiers | ||
| : undefined; | ||
| let flags = ModifierFlags.None; | ||
| if (modifiers) { | ||
| for (const modifier of modifiers) { | ||
| flags |= modifierToFlag(modifier.kind); | ||
| } | ||
| } | ||
| return flags; | ||
| } | ||
|
|
||
| export function getCombinedModifierFlags(node: Declaration): ModifierFlags { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Was excited to use this, but it should be callable for |
||
| let current: Node | undefined = getRootDeclaration(node); | ||
| let flags = getModifierFlags(current); | ||
| if (current.kind === SyntaxKind.VariableDeclaration) { | ||
| current = current.parent; | ||
| } | ||
| if (current?.kind === SyntaxKind.VariableDeclarationList) { | ||
| flags |= getModifierFlags(current); | ||
| current = current.parent; | ||
| } | ||
| if (current?.kind === SyntaxKind.VariableStatement) { | ||
| flags |= getModifierFlags(current); | ||
| } | ||
| return flags; | ||
| } | ||
|
|
||
| function skipParentheses(node: Expression): Expression { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we have instructions or any sort of way to remember / validate that we ported these right into the future?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you mean if we change the implementation in Go?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, right (I chose a random place to comment, sorry)
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should probably add something in the general Copilot instructions, honestly. That will probably help turn a lot of stuff up during automated reviews. The API is so broad that I don't know if there's a good way to get more specific. |
||
| return skipOuterExpressions(node, OuterExpressionKinds.Parentheses); | ||
| } | ||
|
|
||
| function isStringOrNumericLiteralLike(node: Node): node is DeclarationName { | ||
| return isStringLiteralLikeNode(node) || isNumericLiteral(node); | ||
| } | ||
|
|
||
| function getElementOrPropertyAccessName(node: AccessExpression): DeclarationName | undefined { | ||
| if (node.kind === SyntaxKind.PropertyAccessExpression) { | ||
| return isIdentifier(node.name) ? node.name : undefined; | ||
| } | ||
| const argument = skipParentheses(node.argumentExpression); | ||
| return isStringOrNumericLiteralLike(argument) ? argument : undefined; | ||
| } | ||
|
|
||
| function getNameText(node: DeclarationName | undefined): string | undefined { | ||
| return node && "text" in node && typeof node.text === "string" ? node.text : undefined; | ||
| } | ||
|
|
||
| function isEntityNameExpression(node: Node, allowJS = false): boolean { | ||
| if (isIdentifier(node)) return true; | ||
| if (node.kind === SyntaxKind.PropertyAccessExpression) { | ||
| const access = node as PropertyAccessExpression; | ||
| return isIdentifier(access.name) && isEntityNameExpression(access.expression, allowJS); | ||
| } | ||
| if (!allowJS) return false; | ||
| if (node.kind === SyntaxKind.ThisKeyword) return true; | ||
| if (node.kind !== SyntaxKind.ElementAccessExpression) return false; | ||
| const access = node as ElementAccessExpression; | ||
| return isStringOrNumericLiteralLike(access.argumentExpression) && | ||
| isEntityNameExpression(access.expression, true); | ||
| } | ||
|
|
||
| function isBindableStaticAccessExpression(node: Node, excludeThisKeyword: boolean): node is AccessExpression { | ||
| if (node.kind === SyntaxKind.PropertyAccessExpression) { | ||
| const access = node as PropertyAccessExpression; | ||
| return !excludeThisKeyword && access.expression.kind === SyntaxKind.ThisKeyword || | ||
| isIdentifier(access.name) && isBindableStaticNameExpression(access.expression, true); | ||
| } | ||
| if (node.kind !== SyntaxKind.ElementAccessExpression) return false; | ||
| const access = node as ElementAccessExpression; | ||
| return isStringOrNumericLiteralLike(access.argumentExpression) && | ||
| (!excludeThisKeyword && access.expression.kind === SyntaxKind.ThisKeyword || | ||
| isEntityNameExpression(access.expression) || | ||
| isBindableStaticAccessExpression(access.expression, true)); | ||
| } | ||
|
|
||
| function isBindableStaticNameExpression(node: Node, excludeThisKeyword: boolean): boolean { | ||
| return isEntityNameExpression(node) || isBindableStaticAccessExpression(node, excludeThisKeyword); | ||
| } | ||
|
|
||
| function isModuleExportsAccessExpression(node: Node): boolean { | ||
| return isAccessExpression(node) && | ||
| isIdentifier(node.expression) && | ||
| node.expression.text === "module" && | ||
| getNameText(getElementOrPropertyAccessName(node)) === "exports"; | ||
| } | ||
|
|
||
| function isBindableObjectDefinePropertyCall(node: CallExpression): boolean { | ||
| if (node.arguments.length !== 3) return false; | ||
| const expression = node.expression; | ||
| if (expression.kind !== SyntaxKind.PropertyAccessExpression) return false; | ||
| const access = expression as PropertyAccessExpression; | ||
| return isIdentifier(access.expression) && | ||
| access.expression.text === "Object" && | ||
| access.name.text === "defineProperty" && | ||
| isStringOrNumericLiteralLike(node.arguments[1]) && | ||
| isBindableStaticNameExpression(node.arguments[0], true); | ||
| } | ||
|
|
||
| function getAssignmentDeclarationKind(node: BinaryExpression | CallExpression): JSDeclarationKind { | ||
| if (node.kind === SyntaxKind.BinaryExpression) { | ||
| if (node.operatorToken.kind !== SyntaxKind.EqualsToken || !isAccessExpression(node.left)) { | ||
| return JSDeclarationKind.None; | ||
| } | ||
| const left = node.left; | ||
| const isInJSFile = (left.flags & NodeFlags.JavaScriptFile) !== 0; | ||
| if (isInJSFile) { | ||
| if (isModuleExportsAccessExpression(left) && !(isIdentifier(node.right) && node.right.text === "exports")) { | ||
| return JSDeclarationKind.ModuleExports; | ||
| } | ||
| if ( | ||
| (isModuleExportsAccessExpression(left.expression) || | ||
| isIdentifier(left.expression) && left.expression.text === "exports") && | ||
| getElementOrPropertyAccessName(left) | ||
| ) { | ||
| return JSDeclarationKind.ExportsProperty; | ||
| } | ||
| if (left.expression.kind === SyntaxKind.ThisKeyword) { | ||
| return JSDeclarationKind.ThisProperty; | ||
| } | ||
| } | ||
| if ( | ||
| left.kind === SyntaxKind.PropertyAccessExpression && | ||
| isEntityNameExpression(left.expression, isInJSFile) && | ||
| isIdentifier(left.name) || | ||
| left.kind === SyntaxKind.ElementAccessExpression && | ||
| isEntityNameExpression(left.expression, isInJSFile) | ||
| ) { | ||
| return JSDeclarationKind.Property; | ||
| } | ||
| return JSDeclarationKind.None; | ||
| } | ||
|
|
||
| if ( | ||
| (node.flags & NodeFlags.JavaScriptFile) !== 0 && | ||
| isBindableObjectDefinePropertyCall(node) | ||
| ) { | ||
| const entityName = node.arguments[0]; | ||
| return isIdentifier(entityName) && entityName.text === "exports" || | ||
| isModuleExportsAccessExpression(entityName) | ||
| ? JSDeclarationKind.ObjectDefinePropertyExports | ||
| : JSDeclarationKind.ObjectDefinePropertyValue; | ||
| } | ||
| return JSDeclarationKind.None; | ||
| } | ||
|
|
||
| function getDeclaredName(declaration: Declaration | Expression): DeclarationName | undefined { | ||
| return (declaration as (Declaration | Expression) & { readonly name?: DeclarationName; }).name; | ||
| } | ||
|
|
||
| function getNonAssignedNameOfDeclaration(declaration: Declaration | Expression): DeclarationName | undefined { | ||
| if (declaration.kind === SyntaxKind.BinaryExpression || declaration.kind === SyntaxKind.CallExpression) { | ||
| switch (getAssignmentDeclarationKind(declaration as BinaryExpression | CallExpression)) { | ||
| case JSDeclarationKind.Property: | ||
| case JSDeclarationKind.ThisProperty: | ||
| case JSDeclarationKind.ExportsProperty: { | ||
| const left = (declaration as BinaryExpression).left as AccessExpression; | ||
| return getElementOrPropertyAccessName(left) ?? left; | ||
| } | ||
| case JSDeclarationKind.ObjectDefinePropertyValue: | ||
| case JSDeclarationKind.ObjectDefinePropertyExports: | ||
| return (declaration as CallExpression).arguments[1] as DeclarationName; | ||
| default: | ||
| return undefined; | ||
| } | ||
| } | ||
| if (declaration.kind === SyntaxKind.ExportAssignment) { | ||
| const expression = (declaration as ExportAssignment).expression; | ||
| return isIdentifier(expression) ? expression : undefined; | ||
| } | ||
| return getDeclaredName(declaration); | ||
|
Comment on lines
+293
to
+297
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The new behavior is accurate to Go's |
||
| } | ||
|
|
||
| function getAssignedName(node: Node): DeclarationName | undefined { | ||
| const parent = node.parent; | ||
| if (!parent) return undefined; | ||
| switch (parent.kind) { | ||
| case SyntaxKind.PropertyAssignment: | ||
| return (parent as PropertyAssignment).name; | ||
| case SyntaxKind.BindingElement: | ||
| return (parent as BindingElement).name; | ||
| case SyntaxKind.BinaryExpression: { | ||
| const binary = parent as BinaryExpression; | ||
| if (node !== binary.right) return undefined; | ||
| const left = binary.left; | ||
| if (isIdentifier(left)) return left; | ||
| if (left.kind === SyntaxKind.PropertyAccessExpression) return (left as PropertyAccessExpression).name; | ||
| if (left.kind === SyntaxKind.ElementAccessExpression) { | ||
| const argument = skipParentheses((left as ElementAccessExpression).argumentExpression); | ||
| return isStringOrNumericLiteralLike(argument) ? argument : undefined; | ||
| } | ||
| return undefined; | ||
| } | ||
| case SyntaxKind.VariableDeclaration: { | ||
| const name = (parent as VariableDeclaration).name; | ||
| return isIdentifier(name) ? name : undefined; | ||
| } | ||
| default: | ||
| return undefined; | ||
| } | ||
| } | ||
|
|
||
| export function getNameOfDeclaration(declaration: Declaration | Expression | undefined): DeclarationName | undefined { | ||
| if (declaration === undefined) return undefined; | ||
| const nonAssignedName = getNonAssignedNameOfDeclaration(declaration); | ||
| if (nonAssignedName !== undefined) return nonAssignedName; | ||
| return declaration.kind === SyntaxKind.FunctionExpression || | ||
| declaration.kind === SyntaxKind.ArrowFunction || | ||
| declaration.kind === SyntaxKind.ClassExpression | ||
| ? getAssignedName(declaration) | ||
| : undefined; | ||
| } | ||
|
|
||
| export function cloneSourceFileData(sourceFile: SourceFile): Record<string, unknown> { | ||
| return { | ||
| statements: sourceFile.statements, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| // Code generated by tools/scripts/tsc/generate-enums.ts from tsc/internal/ast/utilities.go. DO NOT EDIT. | ||
|
|
||
| export enum JSDeclarationKind { | ||
| None = 0, | ||
| ModuleExports = 1, | ||
| ExportsProperty = 2, | ||
| ThisProperty = 3, | ||
| Property = 4, | ||
| ObjectDefinePropertyValue = 5, | ||
| ObjectDefinePropertyExports = 6, | ||
| } |
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.
We're never going to be able to rid ourselves of that property are we