From 40f0ffaae58f36d14480b30101b5f5618268830d Mon Sep 17 00:00:00 2001 From: fisker Date: Thu, 8 Jan 2026 17:02:31 +0800 Subject: [PATCH 01/16] Refactor with AstVisitor --- src/transform-ast.ts | 83 ++++++ src/transform-node.ts | 451 ------------------------------ src/transform-template-binding.ts | 7 +- src/transform-visitor.ts | 446 +++++++++++++++++++++++++++++ src/transform.ts | 11 +- 5 files changed, 542 insertions(+), 456 deletions(-) create mode 100644 src/transform-ast.ts delete mode 100644 src/transform-node.ts create mode 100644 src/transform-visitor.ts diff --git a/src/transform-ast.ts b/src/transform-ast.ts new file mode 100644 index 00000000..f48a84a6 --- /dev/null +++ b/src/transform-ast.ts @@ -0,0 +1,83 @@ +import * as angular from '@angular/compiler'; + +import { Source } from './source.ts'; +import { transformVisitor } from './transform-visitor.ts'; +import type { NGEmptyExpression, NGNode, RawNGSpan } from './types.ts'; + +class Transformer extends Source { + node: angular.AST; + ancestors: angular.AST[]; + + constructor({ + node, + text, + ancestors, + }: { + node: angular.AST; + text: string; + ancestors: angular.AST[]; + }) { + super(text); + this.node = node; + this.ancestors = ancestors; + } + + create( + properties: Partial & { type: T['type'] }, + location: angular.AST | RawNGSpan | [number, number], + ancestors: angular.AST[], + ) { + const node = super.createNode(properties, location); + + if (ancestors[0] instanceof angular.ParenthesizedExpression) { + node.extra = { + ...node.extra, + parenthesized: true, + }; + } + + return node; + } + + createNode( + properties: Partial & { type: T['type'] }, + location: angular.AST | RawNGSpan | [number, number] = this.node, + ancestorsToCreate: angular.AST[] = this.ancestors, + ) { + return this.create(properties, location, ancestorsToCreate); + } + + transformChild(child: angular.AST) { + return new Transformer({ + node: child, + ancestors: [this.node, ...this.ancestors], + text: this.text, + }).transform(); + } + + transformChildren(children: angular.AST[]) { + return children.map((child) => this.transformChild(child)); + } + + transform() { + const { node } = this; + if (node instanceof angular.EmptyExpr) { + return this.createNode( + { type: 'NGEmptyExpression' }, + node.sourceSpan, + ) as T; + } + + return node.visit(transformVisitor, this) as T; + } + + static transform(node: angular.AST, text: string) { + return new Transformer({ node, text, ancestors: [] }).transform(); + } +} + +const transform = (node: angular.AST, text: string) => { + return Transformer.transform(node, text); +}; + +export { transform, Transformer }; diff --git a/src/transform-node.ts b/src/transform-node.ts deleted file mode 100644 index 73e182b5..00000000 --- a/src/transform-node.ts +++ /dev/null @@ -1,451 +0,0 @@ -import * as angular from '@angular/compiler'; -import type * as babel from '@babel/types'; - -import { Source } from './source.ts'; -import type { - LocationInformation, - NGChainedExpression, - NGEmptyExpression, - NGNode, - NGPipeExpression, - RawNGSpan, -} from './types.ts'; - -function isParenthesized(node: NGNode) { - return Boolean(node.extra?.parenthesized); -} - -function isOptionalObjectOrCallee(node: NGNode): boolean { - if (node.type === 'TSNonNullExpression' && !isParenthesized(node)) { - return isOptionalObjectOrCallee(node.expression); - } - - return ( - (node.type === 'OptionalCallExpression' || - node.type === 'OptionalMemberExpression') && - !isParenthesized(node) - ); -} - -type NodeTransformOptions = { - ancestors: angular.AST[]; -}; - -class Transformer extends Source { - constructor(text: string) { - super(text); - } - - transform( - node: angular.AST, - options?: NodeTransformOptions, - ) { - return this.#transform(node, options ?? { ancestors: [] }) as T & - LocationInformation; - } - - #create( - properties: Partial & { type: T['type'] }, - location: angular.AST | RawNGSpan | [number, number], - ancestors: angular.AST[], - ) { - const node = super.createNode(properties, location); - - if (ancestors[0] instanceof angular.ParenthesizedExpression) { - node.extra = { - ...node.extra, - parenthesized: true, - }; - } - - return node; - } - - #transform(node: angular.AST, options: NodeTransformOptions): NGNode { - const ancestors = options.ancestors; - const transformChild = (child: angular.AST) => - this.transform(child, { ancestors: [node, ...ancestors] }); - const transformChildren = (children: angular.AST[]) => - children.map((child) => transformChild(child)); - const createNode = ( - properties: Partial & { type: T['type'] }, - location: angular.AST | RawNGSpan | [number, number] = node, - ancestorsToCreate: angular.AST[] = ancestors, - ) => this.#create(properties, location, ancestorsToCreate); - - if (node instanceof angular.Interpolation) { - const { expressions } = node; - - /* c8 ignore next 3 @preserve */ - if (expressions.length !== 1) { - throw new Error("Unexpected 'Interpolation'"); - } - - return transformChild(expressions[0]); - } - - if (node instanceof angular.Unary) { - return createNode({ - type: 'UnaryExpression', - prefix: true, - argument: transformChild(node.expr), - operator: node.operator as '-' | '+', - }); - } - - if (node instanceof angular.Binary) { - const { operation: operator } = node; - const [left, right] = transformChildren([ - node.left, - node.right, - ]); - - if (operator === '&&' || operator === '||' || operator === '??') { - return createNode({ - type: 'LogicalExpression', - operator: operator as babel.LogicalExpression['operator'], - left, - right, - }); - } - - if (angular.Binary.isAssignmentOperation(operator)) { - return createNode({ - type: 'AssignmentExpression', - left: left as babel.MemberExpression, - right, - operator: operator as babel.AssignmentExpression['operator'], - }); - } - - return createNode({ - left, - right, - type: 'BinaryExpression', - operator: operator as babel.BinaryExpression['operator'], - }); - } - - if (node instanceof angular.BindingPipe) { - return createNode({ - type: 'NGPipeExpression', - left: transformChild(node.exp), - right: createNode( - { type: 'Identifier', name: node.name }, - node.nameSpan, - ), - arguments: transformChildren(node.args), - }); - } - - if (node instanceof angular.Chain) { - return createNode({ - type: 'NGChainedExpression', - expressions: transformChildren(node.expressions), - }); - } - - if (node instanceof angular.Conditional) { - const [test, consequent, alternate] = transformChildren( - [node.condition, node.trueExp, node.falseExp], - ); - - return createNode({ - type: 'ConditionalExpression', - test, - consequent, - alternate, - }); - } - - if (node instanceof angular.EmptyExpr) { - return createNode({ type: 'NGEmptyExpression' }); - } - - if (node instanceof angular.ThisReceiver) { - return createNode({ type: 'ThisExpression' }); - } - - if (node instanceof angular.LiteralArray) { - return createNode({ - type: 'ArrayExpression', - elements: transformChildren(node.expressions), - }); - } - - if (node instanceof angular.LiteralMap) { - const { keys, values } = node; - const createChild = ( - properties: Partial & { type: T['type'] }, - location: angular.AST | RawNGSpan | [number, number] = node, - ) => this.#create(properties, location, [node, ...ancestors]); - - return createNode({ - type: 'ObjectExpression', - properties: keys.map((keyNode, index) => { - const valueNode = values[index]; - const shorthand = Boolean(keyNode.isShorthandInitialized); - const key = createChild( - keyNode.quoted - ? { type: 'StringLiteral', value: keyNode.key } - : { type: 'Identifier', name: keyNode.key }, - keyNode.sourceSpan, - ); - - return createChild( - { - type: 'ObjectProperty', - key, - value: transformChild(valueNode), - shorthand, - computed: false, - // @ts-expect-error -- Missed in types - method: false, - }, - [keyNode.sourceSpan.start, valueNode.sourceSpan.end], - ); - }), - }); - } - - if (node instanceof angular.LiteralPrimitive) { - const { value } = node; - switch (typeof value) { - case 'boolean': - return createNode({ - type: 'BooleanLiteral', - value, - }); - case 'number': - return createNode({ - type: 'NumericLiteral', - value, - }); - case 'object': - return createNode({ type: 'NullLiteral' }); - case 'string': - return createNode({ - type: 'StringLiteral', - value, - }); - case 'undefined': - return createNode({ - type: 'Identifier', - name: 'undefined', - }); - /* c8 ignore next 4 */ - default: - throw new Error( - `Unexpected LiteralPrimitive value type ${typeof value}`, - ); - } - } - - if (node instanceof angular.RegularExpressionLiteral) { - return createNode({ - type: 'RegExpLiteral', - pattern: node.body, - flags: node.flags ?? '', - }); - } - - if (node instanceof angular.Call || node instanceof angular.SafeCall) { - const arguments_ = transformChildren(node.args); - const callee = transformChild(node.receiver); - const isOptionalReceiver = isOptionalObjectOrCallee(callee); - const isOptional = node instanceof angular.SafeCall; - const nodeType = - isOptional || isOptionalReceiver - ? 'OptionalCallExpression' - : 'CallExpression'; - return createNode({ - type: nodeType, - callee, - arguments: arguments_, - ...(nodeType === 'OptionalCallExpression' - ? { optional: isOptional } - : undefined), - }); - } - - if (node instanceof angular.NonNullAssert) { - return createNode({ - type: 'TSNonNullExpression', - expression: transformChild(node.expression), - }); - } - - if (node instanceof angular.PrefixNot) { - return createNode( - { - type: 'UnaryExpression', - prefix: true, - operator: '!', - argument: transformChild(node.expression), - }, - node.sourceSpan, - ); - } - - if (node instanceof angular.TypeofExpression) { - return createNode( - { - type: 'UnaryExpression', - prefix: true, - operator: 'typeof', - argument: transformChild(node.expression), - }, - node.sourceSpan, - ); - } - - if (node instanceof angular.VoidExpression) { - return createNode( - { - type: 'UnaryExpression', - prefix: true, - operator: 'void', - argument: transformChild(node.expression), - }, - node.sourceSpan, - ); - } - - if ( - node instanceof angular.KeyedRead || - node instanceof angular.SafeKeyedRead || - node instanceof angular.PropertyRead || - node instanceof angular.SafePropertyRead - ) { - const isComputed = - node instanceof angular.KeyedRead || - node instanceof angular.SafeKeyedRead; - const isOptional = - node instanceof angular.SafeKeyedRead || - node instanceof angular.SafePropertyRead; - const { receiver } = node; - const isImplicitReceiver = receiver instanceof angular.ImplicitReceiver; - - let property; - if (isComputed) { - property = transformChild(node.key); - } else { - property = createNode( - { type: 'Identifier', name: node.name }, - node.nameSpan, - isImplicitReceiver ? ancestors : [], - ); - } - - if (isImplicitReceiver) { - return property; - } - - const object = transformChild(receiver); - const isOptionalObject = isOptionalObjectOrCallee(object); - - if (isOptional || isOptionalObject) { - return createNode({ - type: 'OptionalMemberExpression', - optional: isOptional || !isOptionalObject, - computed: isComputed, - property, - object, - }); - } - - if (isComputed) { - return createNode({ - type: 'MemberExpression', - property, - object, - computed: true, - }); - } - - return createNode({ - type: 'MemberExpression', - object, - property: property as babel.MemberExpressionNonComputed['property'], - computed: false, - }); - } - - if (node instanceof angular.TaggedTemplateLiteral) { - return createNode({ - type: 'TaggedTemplateExpression', - tag: transformChild(node.tag), - quasi: transformChild(node.template), - }); - } - - if (node instanceof angular.TemplateLiteral) { - return createNode({ - type: 'TemplateLiteral', - quasis: transformChildren(node.elements), - expressions: transformChildren(node.expressions), - }); - } - - if (node instanceof angular.TemplateLiteralElement) { - const [parent] = ancestors; - const { elements } = parent as angular.TemplateLiteral; - const elementIndex = elements.indexOf(node); - const isFirst = elementIndex === 0; - const isLast = elementIndex === elements.length - 1; - - const end = node.sourceSpan.end - (isLast ? 1 : 0); - const start = node.sourceSpan.start + (isFirst ? 1 : 0); - const raw = this.text.slice(start, end); - - return createNode( - { - type: 'TemplateElement', - value: { cooked: node.text, raw }, - tail: isLast, - }, - [start, end], - ); - } - - if (node instanceof angular.ParenthesizedExpression) { - return transformChild(node.expression); - } - - /* c8 ignore next @preserve */ - throw new Error(`Unexpected node type '${node.constructor.name}'`); - } -} - -// See `convertAst` in `@angular/compiler` -type SupportedNodes = - | angular.ASTWithSource // Not handled - | angular.PropertyRead - | angular.Call - | angular.LiteralPrimitive - | angular.Unary - | angular.Binary - | angular.ThisReceiver - | angular.ImplicitReceiver - | angular.KeyedRead - | angular.Chain - | angular.LiteralMap - | angular.LiteralArray - | angular.Conditional - | angular.NonNullAssert - | angular.BindingPipe - | angular.SafeKeyedRead - | angular.SafePropertyRead - | angular.SafeCall - | angular.EmptyExpr - | angular.PrefixNot - | angular.TypeofExpression - | angular.VoidExpression - | angular.TemplateLiteral // Including `TemplateLiteralElement` - | angular.TaggedTemplateLiteral - | angular.ParenthesizedExpression; -function transform(node: SupportedNodes, text: string): NGNode { - return new Transformer(text).transform(node); -} - -export { transform, Transformer }; diff --git a/src/transform-template-binding.ts b/src/transform-template-binding.ts index 8a9c6004..6737a7f6 100644 --- a/src/transform-template-binding.ts +++ b/src/transform-template-binding.ts @@ -4,7 +4,8 @@ import { VariableBinding as NGVariableBinding, } from '@angular/compiler'; -import { Transformer as NodeTransformer } from './transform-node.ts'; +import { Source } from './source.ts'; +import { transform as transformNode } from './transform-ast.ts'; import type { NGMicrosyntax, NGMicrosyntaxAs, @@ -30,7 +31,7 @@ function isVariableBinding( return templateBinding instanceof NGVariableBinding; } -class TemplateBindingTransformer extends NodeTransformer { +class TemplateBindingTransformer extends Source { #rawTemplateBindings; #text; @@ -61,7 +62,7 @@ class TemplateBindingTransformer extends NodeTransformer { } #transform(node: angular.AST) { - return super.transform(node) as T; + return transformNode(node, this.text) as T; } #removePrefix(string: string) { diff --git a/src/transform-visitor.ts b/src/transform-visitor.ts new file mode 100644 index 00000000..905458cc --- /dev/null +++ b/src/transform-visitor.ts @@ -0,0 +1,446 @@ +import * as angular from '@angular/compiler'; +import type * as babel from '@babel/types'; + +import { type Transformer } from './transform-ast.ts'; +import type { + NGChainedExpression, + NGNode, + NGPipeExpression, + RawNGSpan, +} from './types.ts'; + +function isParenthesized(node: NGNode) { + return Boolean(node.extra?.parenthesized); +} + +function isOptionalObjectOrCallee(node: NGNode): boolean { + if (node.type === 'TSNonNullExpression' && !isParenthesized(node)) { + return isOptionalObjectOrCallee(node.expression); + } + + return ( + (node.type === 'OptionalCallExpression' || + node.type === 'OptionalMemberExpression') && + !isParenthesized(node) + ); +} + +class TransformerVisitor implements angular.AstVisitor { + visitUnary(node: angular.Unary, transformer: Transformer) { + return transformer.createNode({ + type: 'UnaryExpression', + prefix: true, + argument: transformer.transformChild(node.expr), + operator: node.operator as '-' | '+', + }); + } + + visitBinary(node: angular.Binary, transformer: Transformer) { + const { operation: operator } = node; + const [left, right] = transformer.transformChildren([ + node.left, + node.right, + ]); + + if (operator === '&&' || operator === '||' || operator === '??') { + return transformer.createNode({ + type: 'LogicalExpression', + operator: operator as babel.LogicalExpression['operator'], + left, + right, + }); + } + + if (angular.Binary.isAssignmentOperation(operator)) { + return transformer.createNode({ + type: 'AssignmentExpression', + left: left as babel.MemberExpression, + right, + operator: operator as babel.AssignmentExpression['operator'], + }); + } + + return transformer.createNode({ + left, + right, + type: 'BinaryExpression', + operator: operator as babel.BinaryExpression['operator'], + }); + } + + visitPipe(node: angular.BindingPipe, transformer: Transformer) { + return transformer.createNode({ + type: 'NGPipeExpression', + left: transformer.transformChild(node.exp), + right: transformer.createNode( + { type: 'Identifier', name: node.name }, + node.nameSpan, + ), + arguments: transformer.transformChildren(node.args), + }); + } + + visitChain(node: angular.Chain, transformer: Transformer) { + return transformer.createNode({ + type: 'NGChainedExpression', + expressions: transformer.transformChildren( + node.expressions, + ), + }); + } + + visitConditional(node: angular.Conditional, transformer: Transformer) { + const [test, consequent, alternate] = + transformer.transformChildren([ + node.condition, + node.trueExp, + node.falseExp, + ]); + + return transformer.createNode({ + type: 'ConditionalExpression', + test, + consequent, + alternate, + }); + } + + visitThisReceiver(node: angular.ThisReceiver, transformer: Transformer) { + return transformer.createNode({ + type: 'ThisExpression', + }); + } + + visitLiteralArray(node: angular.LiteralArray, transformer: Transformer) { + return transformer.createNode({ + type: 'ArrayExpression', + elements: transformer.transformChildren( + node.expressions, + ), + }); + } + + visitLiteralMap(node: angular.LiteralMap, transformer: Transformer) { + const { keys, values } = node; + const createChild = ( + properties: Partial & { type: T['type'] }, + location: angular.AST | RawNGSpan | [number, number] = node, + ) => + transformer.create(properties, location, [ + node, + ...transformer.ancestors, + ]); + + return transformer.createNode({ + type: 'ObjectExpression', + properties: keys.map((keyNode, index) => { + const valueNode = values[index]; + const shorthand = Boolean(keyNode.isShorthandInitialized); + const key = createChild( + keyNode.quoted + ? { type: 'StringLiteral', value: keyNode.key } + : { type: 'Identifier', name: keyNode.key }, + keyNode.sourceSpan, + ); + + return createChild( + { + type: 'ObjectProperty', + key, + value: transformer.transformChild(valueNode), + shorthand, + computed: false, + // @ts-expect-error -- Missed in types + method: false, + }, + [keyNode.sourceSpan.start, valueNode.sourceSpan.end], + ); + }), + }); + } + + visitLiteralPrimitive( + node: angular.LiteralPrimitive, + transformer: Transformer, + ) { + const { value } = node; + switch (typeof value) { + case 'boolean': + return transformer.createNode({ + type: 'BooleanLiteral', + value, + }); + case 'number': + return transformer.createNode({ + type: 'NumericLiteral', + value, + }); + case 'object': + return transformer.createNode({ + type: 'NullLiteral', + }); + case 'string': + return transformer.createNode({ + type: 'StringLiteral', + value, + }); + case 'undefined': + return transformer.createNode({ + type: 'Identifier', + name: 'undefined', + }); + /* c8 ignore next 4 */ + default: + throw new Error( + `Unexpected LiteralPrimitive value type ${typeof value}`, + ); + } + } + + visitRegularExpressionLiteral( + node: angular.RegularExpressionLiteral, + transformer: Transformer, + ) { + return transformer.createNode({ + type: 'RegExpLiteral', + pattern: node.body, + flags: node.flags ?? '', + }); + } + + visitNonNullAssert(node: angular.NonNullAssert, transformer: Transformer) { + return transformer.createNode({ + type: 'TSNonNullExpression', + expression: transformer.transformChild(node.expression), + }); + } + + visitPrefixNot(node: angular.PrefixNot, transformer: Transformer) { + return transformer.createNode( + { + type: 'UnaryExpression', + prefix: true, + operator: '!', + argument: transformer.transformChild(node.expression), + }, + node.sourceSpan, + ); + } + + visitTypeofExpression( + node: angular.TypeofExpression, + transformer: Transformer, + ) { + return transformer.createNode( + { + type: 'UnaryExpression', + prefix: true, + operator: 'typeof', + argument: transformer.transformChild(node.expression), + }, + node.sourceSpan, + ); + } + + visitVoidExpression( + node: angular.TypeofExpression, + transformer: Transformer, + ) { + return transformer.createNode( + { + type: 'UnaryExpression', + prefix: true, + operator: 'void', + argument: transformer.transformChild(node.expression), + }, + node.sourceSpan, + ); + } + + visitTaggedTemplateLiteral( + node: angular.TaggedTemplateLiteral, + transformer: Transformer, + ) { + return transformer.createNode({ + type: 'TaggedTemplateExpression', + tag: transformer.transformChild(node.tag), + quasi: transformer.transformChild(node.template), + }); + } + + visitTemplateLiteral( + node: angular.TemplateLiteral, + transformer: Transformer, + ) { + return transformer.createNode({ + type: 'TemplateLiteral', + quasis: transformer.transformChildren(node.elements), + expressions: transformer.transformChildren(node.expressions), + }); + } + + visitTemplateLiteralElement( + node: angular.TemplateLiteralElement, + transformer: Transformer, + ) { + const [parent] = transformer.ancestors; + const { elements } = parent as angular.TemplateLiteral; + const elementIndex = elements.indexOf(node); + const isFirst = elementIndex === 0; + const isLast = elementIndex === elements.length - 1; + + const end = node.sourceSpan.end - (isLast ? 1 : 0); + const start = node.sourceSpan.start + (isFirst ? 1 : 0); + const raw = transformer.text.slice(start, end); + + return transformer.createNode( + { + type: 'TemplateElement', + value: { cooked: node.text, raw }, + tail: isLast, + }, + [start, end], + ); + } + + visitParenthesizedExpression( + node: angular.ParenthesizedExpression, + transformer: Transformer, + ) { + return transformer.transformChild(node.expression); + } + + #visitRead( + node: + | angular.KeyedRead + | angular.SafeKeyedRead + | angular.PropertyRead + | angular.SafePropertyRead, + transformer: Transformer, + ) { + const isComputed = + node instanceof angular.KeyedRead || + node instanceof angular.SafeKeyedRead; + const isOptional = + node instanceof angular.SafeKeyedRead || + node instanceof angular.SafePropertyRead; + const { receiver } = node; + const isImplicitReceiver = receiver instanceof angular.ImplicitReceiver; + + let property; + if (isComputed) { + property = transformer.transformChild(node.key); + } else { + property = transformer.createNode( + { type: 'Identifier', name: node.name }, + node.nameSpan, + isImplicitReceiver ? transformer.ancestors : [], + ); + } + + if (isImplicitReceiver) { + return property; + } + + const object = transformer.transformChild(receiver); + const isOptionalObject = isOptionalObjectOrCallee(object); + + if (isOptional || isOptionalObject) { + return transformer.createNode({ + type: 'OptionalMemberExpression', + optional: isOptional || !isOptionalObject, + computed: isComputed, + property, + object, + }); + } + + if (isComputed) { + return transformer.createNode({ + type: 'MemberExpression', + property, + object, + computed: true, + }); + } + + return transformer.createNode({ + type: 'MemberExpression', + object, + property: property as babel.MemberExpressionNonComputed['property'], + computed: false, + }); + } + + visitKeyedRead(node: angular.KeyedRead, transformer: Transformer) { + return this.#visitRead(node, transformer); + } + + visitSafeKeyedRead(node: angular.SafeKeyedRead, transformer: Transformer) { + return this.#visitRead(node, transformer); + } + + visitPropertyRead(node: angular.PropertyRead, transformer: Transformer) { + return this.#visitRead(node, transformer); + } + + visitSafePropertyRead( + node: angular.SafePropertyRead, + transformer: Transformer, + ) { + return this.#visitRead(node, transformer); + } + + #visitCall(node: angular.Call | angular.SafeCall, transformer: Transformer) { + const arguments_ = transformer.transformChildren( + node.args, + ); + const callee = transformer.transformChild(node.receiver); + const isOptionalReceiver = isOptionalObjectOrCallee(callee); + const isOptional = node instanceof angular.SafeCall; + const nodeType = + isOptional || isOptionalReceiver + ? 'OptionalCallExpression' + : 'CallExpression'; + return transformer.createNode< + babel.CallExpression | babel.OptionalCallExpression + >({ + type: nodeType, + callee, + arguments: arguments_, + ...(nodeType === 'OptionalCallExpression' + ? { optional: isOptional } + : undefined), + }); + } + + visitCall(node: angular.Call, transformer: Transformer) { + return this.#visitCall(node, transformer); + } + + visitSafeCall(node: angular.SafeCall, transformer: Transformer) { + return this.#visitCall(node, transformer); + } + + visitInterpolation(node: angular.Interpolation, transformer: Transformer) { + const { expressions } = node; + + /* c8 ignore next 3 @preserve */ + if (expressions.length !== 1) { + throw new Error("Unexpected 'Interpolation'"); + } + + return transformer.transformChild(expressions[0]); + } + + visit(node: angular.AST, context?: any) {} + + visitASTWithSource(node: angular.ASTWithSource, transformer: Transformer) {} + + visitImplicitReceiver( + node: angular.ImplicitReceiver, + transformer: Transformer, + ) {} +} + +export const transformVisitor = new TransformerVisitor(); diff --git a/src/transform.ts b/src/transform.ts index 79313f76..a1fc7c26 100644 --- a/src/transform.ts +++ b/src/transform.ts @@ -2,7 +2,7 @@ import type { AstParseResult, MicroSyntaxParseResult, } from './angular-parser.ts'; -import { transform as transformNode } from './transform-node.ts'; +import { transform as transformNode } from './transform-ast.ts'; import { transform as transformTemplateBindings } from './transform-template-binding.ts'; function transformAstResult({ @@ -10,7 +10,14 @@ function transformAstResult({ text, comments, }: AstParseResult) { - return Object.assign(transformNode(ast, text), { comments }); + try { + return Object.assign(transformNode(ast, text), { comments }); + } catch { + console.log({ + ast, + es: transformNode(ast, text), + }); + } } function transformMicrosyntaxResult({ From 09399e122b545e556da3a7fae816cb6e01f366d8 Mon Sep 17 00:00:00 2001 From: fisker Date: Thu, 8 Jan 2026 18:37:10 +0800 Subject: [PATCH 02/16] Refactor --- src/transform-visitor.ts | 243 +++++++++++++++++++-------------------- 1 file changed, 119 insertions(+), 124 deletions(-) diff --git a/src/transform-visitor.ts b/src/transform-visitor.ts index 905458cc..5975f14c 100644 --- a/src/transform-visitor.ts +++ b/src/transform-visitor.ts @@ -25,7 +25,11 @@ function isOptionalObjectOrCallee(node: NGNode): boolean { ); } -class TransformerVisitor implements angular.AstVisitor { +type AstVisitor = Required< + Omit +>; + +export const transformVisitor: AstVisitor = { visitUnary(node: angular.Unary, transformer: Transformer) { return transformer.createNode({ type: 'UnaryExpression', @@ -33,7 +37,7 @@ class TransformerVisitor implements angular.AstVisitor { argument: transformer.transformChild(node.expr), operator: node.operator as '-' | '+', }); - } + }, visitBinary(node: angular.Binary, transformer: Transformer) { const { operation: operator } = node; @@ -66,7 +70,7 @@ class TransformerVisitor implements angular.AstVisitor { type: 'BinaryExpression', operator: operator as babel.BinaryExpression['operator'], }); - } + }, visitPipe(node: angular.BindingPipe, transformer: Transformer) { return transformer.createNode({ @@ -78,7 +82,7 @@ class TransformerVisitor implements angular.AstVisitor { ), arguments: transformer.transformChildren(node.args), }); - } + }, visitChain(node: angular.Chain, transformer: Transformer) { return transformer.createNode({ @@ -87,7 +91,7 @@ class TransformerVisitor implements angular.AstVisitor { node.expressions, ), }); - } + }, visitConditional(node: angular.Conditional, transformer: Transformer) { const [test, consequent, alternate] = @@ -103,13 +107,13 @@ class TransformerVisitor implements angular.AstVisitor { consequent, alternate, }); - } + }, visitThisReceiver(node: angular.ThisReceiver, transformer: Transformer) { return transformer.createNode({ type: 'ThisExpression', }); - } + }, visitLiteralArray(node: angular.LiteralArray, transformer: Transformer) { return transformer.createNode({ @@ -118,7 +122,7 @@ class TransformerVisitor implements angular.AstVisitor { node.expressions, ), }); - } + }, visitLiteralMap(node: angular.LiteralMap, transformer: Transformer) { const { keys, values } = node; @@ -157,7 +161,7 @@ class TransformerVisitor implements angular.AstVisitor { ); }), }); - } + }, visitLiteralPrimitive( node: angular.LiteralPrimitive, @@ -195,7 +199,7 @@ class TransformerVisitor implements angular.AstVisitor { `Unexpected LiteralPrimitive value type ${typeof value}`, ); } - } + }, visitRegularExpressionLiteral( node: angular.RegularExpressionLiteral, @@ -206,14 +210,14 @@ class TransformerVisitor implements angular.AstVisitor { pattern: node.body, flags: node.flags ?? '', }); - } + }, visitNonNullAssert(node: angular.NonNullAssert, transformer: Transformer) { return transformer.createNode({ type: 'TSNonNullExpression', expression: transformer.transformChild(node.expression), }); - } + }, visitPrefixNot(node: angular.PrefixNot, transformer: Transformer) { return transformer.createNode( @@ -225,7 +229,7 @@ class TransformerVisitor implements angular.AstVisitor { }, node.sourceSpan, ); - } + }, visitTypeofExpression( node: angular.TypeofExpression, @@ -240,7 +244,7 @@ class TransformerVisitor implements angular.AstVisitor { }, node.sourceSpan, ); - } + }, visitVoidExpression( node: angular.TypeofExpression, @@ -255,7 +259,7 @@ class TransformerVisitor implements angular.AstVisitor { }, node.sourceSpan, ); - } + }, visitTaggedTemplateLiteral( node: angular.TaggedTemplateLiteral, @@ -266,7 +270,7 @@ class TransformerVisitor implements angular.AstVisitor { tag: transformer.transformChild(node.tag), quasi: transformer.transformChild(node.template), }); - } + }, visitTemplateLiteral( node: angular.TemplateLiteral, @@ -277,7 +281,7 @@ class TransformerVisitor implements angular.AstVisitor { quasis: transformer.transformChildren(node.elements), expressions: transformer.transformChildren(node.expressions), }); - } + }, visitTemplateLiteralElement( node: angular.TemplateLiteralElement, @@ -301,126 +305,44 @@ class TransformerVisitor implements angular.AstVisitor { }, [start, end], ); - } + }, visitParenthesizedExpression( node: angular.ParenthesizedExpression, transformer: Transformer, ) { return transformer.transformChild(node.expression); - } - - #visitRead( - node: - | angular.KeyedRead - | angular.SafeKeyedRead - | angular.PropertyRead - | angular.SafePropertyRead, - transformer: Transformer, - ) { - const isComputed = - node instanceof angular.KeyedRead || - node instanceof angular.SafeKeyedRead; - const isOptional = - node instanceof angular.SafeKeyedRead || - node instanceof angular.SafePropertyRead; - const { receiver } = node; - const isImplicitReceiver = receiver instanceof angular.ImplicitReceiver; - - let property; - if (isComputed) { - property = transformer.transformChild(node.key); - } else { - property = transformer.createNode( - { type: 'Identifier', name: node.name }, - node.nameSpan, - isImplicitReceiver ? transformer.ancestors : [], - ); - } - - if (isImplicitReceiver) { - return property; - } - - const object = transformer.transformChild(receiver); - const isOptionalObject = isOptionalObjectOrCallee(object); - - if (isOptional || isOptionalObject) { - return transformer.createNode({ - type: 'OptionalMemberExpression', - optional: isOptional || !isOptionalObject, - computed: isComputed, - property, - object, - }); - } - - if (isComputed) { - return transformer.createNode({ - type: 'MemberExpression', - property, - object, - computed: true, - }); - } - - return transformer.createNode({ - type: 'MemberExpression', - object, - property: property as babel.MemberExpressionNonComputed['property'], - computed: false, - }); - } + }, visitKeyedRead(node: angular.KeyedRead, transformer: Transformer) { - return this.#visitRead(node, transformer); - } + return transformMemberExpression(node, transformer, { computed: true }); + }, visitSafeKeyedRead(node: angular.SafeKeyedRead, transformer: Transformer) { - return this.#visitRead(node, transformer); - } + return transformMemberExpression(node, transformer, { + computed: true, + optional: true, + }); + }, visitPropertyRead(node: angular.PropertyRead, transformer: Transformer) { - return this.#visitRead(node, transformer); - } + return transformMemberExpression(node, transformer); + }, visitSafePropertyRead( node: angular.SafePropertyRead, transformer: Transformer, ) { - return this.#visitRead(node, transformer); - } - - #visitCall(node: angular.Call | angular.SafeCall, transformer: Transformer) { - const arguments_ = transformer.transformChildren( - node.args, - ); - const callee = transformer.transformChild(node.receiver); - const isOptionalReceiver = isOptionalObjectOrCallee(callee); - const isOptional = node instanceof angular.SafeCall; - const nodeType = - isOptional || isOptionalReceiver - ? 'OptionalCallExpression' - : 'CallExpression'; - return transformer.createNode< - babel.CallExpression | babel.OptionalCallExpression - >({ - type: nodeType, - callee, - arguments: arguments_, - ...(nodeType === 'OptionalCallExpression' - ? { optional: isOptional } - : undefined), - }); - } + return transformMemberExpression(node, transformer, { optional: true }); + }, visitCall(node: angular.Call, transformer: Transformer) { - return this.#visitCall(node, transformer); - } + return transformCall(node, transformer); + }, visitSafeCall(node: angular.SafeCall, transformer: Transformer) { - return this.#visitCall(node, transformer); - } + return transformCall(node, transformer, { optional: true }); + }, visitInterpolation(node: angular.Interpolation, transformer: Transformer) { const { expressions } = node; @@ -431,16 +353,89 @@ class TransformerVisitor implements angular.AstVisitor { } return transformer.transformChild(expressions[0]); + }, + + visitImplicitReceiver() {}, +}; + +function transformCall( + node: angular.Call | angular.SafeCall, + transformer: Transformer, + { optional = false } = {}, +) { + const arguments_ = transformer.transformChildren(node.args); + const callee = transformer.transformChild(node.receiver); + const isOptionalReceiver = isOptionalObjectOrCallee(callee); + const nodeType = + optional || isOptionalReceiver + ? 'OptionalCallExpression' + : 'CallExpression'; + return transformer.createNode< + babel.CallExpression | babel.OptionalCallExpression + >({ + type: nodeType, + callee, + arguments: arguments_, + ...(nodeType === 'OptionalCallExpression' ? { optional } : undefined), + }); +} + +function transformMemberExpression( + node: + | angular.KeyedRead + | angular.SafeKeyedRead + | angular.PropertyRead + | angular.SafePropertyRead, + transformer: Transformer, +) { + const { receiver } = node; + const object = transformer.transformChild(receiver); + const computed = + node instanceof angular.KeyedRead || node instanceof angular.SafeKeyedRead; + const optional = + node instanceof angular.SafeKeyedRead || + node instanceof angular.SafePropertyRead; + + let property; + if (computed) { + property = transformer.transformChild(node.key); + } else { + property = transformer.createNode( + { type: 'Identifier', name: node.name }, + node.nameSpan, + object ? [] : transformer.ancestors, + ); } - visit(node: angular.AST, context?: any) {} + if (!object) { + return property; + } - visitASTWithSource(node: angular.ASTWithSource, transformer: Transformer) {} + const isOptionalObject = isOptionalObjectOrCallee(object); - visitImplicitReceiver( - node: angular.ImplicitReceiver, - transformer: Transformer, - ) {} -} + if (optional || isOptionalObject) { + return transformer.createNode({ + type: 'OptionalMemberExpression', + optional: optional || !isOptionalObject, + computed, + property, + object, + }); + } + + if (computed) { + return transformer.createNode({ + type: 'MemberExpression', + property, + object, + computed: true, + }); + } -export const transformVisitor = new TransformerVisitor(); + return transformer.createNode({ + type: 'MemberExpression', + object, + property: property as babel.MemberExpressionNonComputed['property'], + computed: false, + }); +} From c5d4695569f39f36ae2204268b30f3802e5f5301 Mon Sep 17 00:00:00 2001 From: fisker Date: Thu, 8 Jan 2026 21:36:49 +0800 Subject: [PATCH 03/16] Refactor --- src/transform-visitor.ts | 203 +++--------------- src/transform.ts | 9 +- src/transforms/transform-call.ts | 44 ++++ src/transforms/transform-member-expression.ts | 101 +++++++++ src/transforms/transform-unary-expression.ts | 55 +++++ src/transforms/utilities.ts | 17 ++ 6 files changed, 243 insertions(+), 186 deletions(-) create mode 100644 src/transforms/transform-call.ts create mode 100644 src/transforms/transform-member-expression.ts create mode 100644 src/transforms/transform-unary-expression.ts create mode 100644 src/transforms/utilities.ts diff --git a/src/transform-visitor.ts b/src/transform-visitor.ts index 5975f14c..588bcf8a 100644 --- a/src/transform-visitor.ts +++ b/src/transform-visitor.ts @@ -2,6 +2,19 @@ import * as angular from '@angular/compiler'; import type * as babel from '@babel/types'; import { type Transformer } from './transform-ast.ts'; +import { visitCall, visitSafeCall } from './transforms/transform-call.ts'; +import { + visitKeyedRead, + visitPropertyRead, + visitSafeKeyedRead, + visitSafePropertyRead, +} from './transforms/transform-member-expression.ts'; +import { + visitPrefixNot, + visitTypeofExpression, + visitUnary, + visitVoidExpression, +} from './transforms/transform-unary-expression.ts'; import type { NGChainedExpression, NGNode, @@ -9,35 +22,23 @@ import type { RawNGSpan, } from './types.ts'; -function isParenthesized(node: NGNode) { - return Boolean(node.extra?.parenthesized); -} - -function isOptionalObjectOrCallee(node: NGNode): boolean { - if (node.type === 'TSNonNullExpression' && !isParenthesized(node)) { - return isOptionalObjectOrCallee(node.expression); - } - - return ( - (node.type === 'OptionalCallExpression' || - node.type === 'OptionalMemberExpression') && - !isParenthesized(node) - ); -} - type AstVisitor = Required< Omit >; export const transformVisitor: AstVisitor = { - visitUnary(node: angular.Unary, transformer: Transformer) { - return transformer.createNode({ - type: 'UnaryExpression', - prefix: true, - argument: transformer.transformChild(node.expr), - operator: node.operator as '-' | '+', - }); - }, + visitCall, + visitSafeCall, + + visitKeyedRead, + visitPropertyRead, + visitSafeKeyedRead, + visitSafePropertyRead, + + visitPrefixNot, + visitTypeofExpression, + visitVoidExpression, + visitUnary, visitBinary(node: angular.Binary, transformer: Transformer) { const { operation: operator } = node; @@ -219,48 +220,6 @@ export const transformVisitor: AstVisitor = { }); }, - visitPrefixNot(node: angular.PrefixNot, transformer: Transformer) { - return transformer.createNode( - { - type: 'UnaryExpression', - prefix: true, - operator: '!', - argument: transformer.transformChild(node.expression), - }, - node.sourceSpan, - ); - }, - - visitTypeofExpression( - node: angular.TypeofExpression, - transformer: Transformer, - ) { - return transformer.createNode( - { - type: 'UnaryExpression', - prefix: true, - operator: 'typeof', - argument: transformer.transformChild(node.expression), - }, - node.sourceSpan, - ); - }, - - visitVoidExpression( - node: angular.TypeofExpression, - transformer: Transformer, - ) { - return transformer.createNode( - { - type: 'UnaryExpression', - prefix: true, - operator: 'void', - argument: transformer.transformChild(node.expression), - }, - node.sourceSpan, - ); - }, - visitTaggedTemplateLiteral( node: angular.TaggedTemplateLiteral, transformer: Transformer, @@ -314,36 +273,6 @@ export const transformVisitor: AstVisitor = { return transformer.transformChild(node.expression); }, - visitKeyedRead(node: angular.KeyedRead, transformer: Transformer) { - return transformMemberExpression(node, transformer, { computed: true }); - }, - - visitSafeKeyedRead(node: angular.SafeKeyedRead, transformer: Transformer) { - return transformMemberExpression(node, transformer, { - computed: true, - optional: true, - }); - }, - - visitPropertyRead(node: angular.PropertyRead, transformer: Transformer) { - return transformMemberExpression(node, transformer); - }, - - visitSafePropertyRead( - node: angular.SafePropertyRead, - transformer: Transformer, - ) { - return transformMemberExpression(node, transformer, { optional: true }); - }, - - visitCall(node: angular.Call, transformer: Transformer) { - return transformCall(node, transformer); - }, - - visitSafeCall(node: angular.SafeCall, transformer: Transformer) { - return transformCall(node, transformer, { optional: true }); - }, - visitInterpolation(node: angular.Interpolation, transformer: Transformer) { const { expressions } = node; @@ -357,85 +286,3 @@ export const transformVisitor: AstVisitor = { visitImplicitReceiver() {}, }; - -function transformCall( - node: angular.Call | angular.SafeCall, - transformer: Transformer, - { optional = false } = {}, -) { - const arguments_ = transformer.transformChildren(node.args); - const callee = transformer.transformChild(node.receiver); - const isOptionalReceiver = isOptionalObjectOrCallee(callee); - const nodeType = - optional || isOptionalReceiver - ? 'OptionalCallExpression' - : 'CallExpression'; - return transformer.createNode< - babel.CallExpression | babel.OptionalCallExpression - >({ - type: nodeType, - callee, - arguments: arguments_, - ...(nodeType === 'OptionalCallExpression' ? { optional } : undefined), - }); -} - -function transformMemberExpression( - node: - | angular.KeyedRead - | angular.SafeKeyedRead - | angular.PropertyRead - | angular.SafePropertyRead, - transformer: Transformer, -) { - const { receiver } = node; - const object = transformer.transformChild(receiver); - const computed = - node instanceof angular.KeyedRead || node instanceof angular.SafeKeyedRead; - const optional = - node instanceof angular.SafeKeyedRead || - node instanceof angular.SafePropertyRead; - - let property; - if (computed) { - property = transformer.transformChild(node.key); - } else { - property = transformer.createNode( - { type: 'Identifier', name: node.name }, - node.nameSpan, - object ? [] : transformer.ancestors, - ); - } - - if (!object) { - return property; - } - - const isOptionalObject = isOptionalObjectOrCallee(object); - - if (optional || isOptionalObject) { - return transformer.createNode({ - type: 'OptionalMemberExpression', - optional: optional || !isOptionalObject, - computed, - property, - object, - }); - } - - if (computed) { - return transformer.createNode({ - type: 'MemberExpression', - property, - object, - computed: true, - }); - } - - return transformer.createNode({ - type: 'MemberExpression', - object, - property: property as babel.MemberExpressionNonComputed['property'], - computed: false, - }); -} diff --git a/src/transform.ts b/src/transform.ts index a1fc7c26..98ac70cd 100644 --- a/src/transform.ts +++ b/src/transform.ts @@ -10,14 +10,7 @@ function transformAstResult({ text, comments, }: AstParseResult) { - try { - return Object.assign(transformNode(ast, text), { comments }); - } catch { - console.log({ - ast, - es: transformNode(ast, text), - }); - } + return Object.assign(transformNode(ast, text), { comments }); } function transformMicrosyntaxResult({ diff --git a/src/transforms/transform-call.ts b/src/transforms/transform-call.ts new file mode 100644 index 00000000..ef835378 --- /dev/null +++ b/src/transforms/transform-call.ts @@ -0,0 +1,44 @@ +import { type Call, type SafeCall } from '@angular/compiler'; +import type * as babel from '@babel/types'; + +import { type Transformer } from '../transform-ast.ts'; +import { isOptionalObjectOrCallee } from './utilities.ts'; + +const callOptions = { optional: false } as const; +const safeCallOptions = { optional: true } as const; + +type VisitorCall = { + node: Call; + options: typeof callOptions; +}; +type VisitorSafeCall = { + node: SafeCall; + options: typeof safeCallOptions; +}; + +const transformCall = + ({ + optional, + }: Visitor['options']) => + (node: Visitor['node'], transformer: Transformer) => { + const arguments_ = transformer.transformChildren( + node.args, + ); + const callee = transformer.transformChild(node.receiver); + const isOptionalReceiver = isOptionalObjectOrCallee(callee); + const nodeType = + optional || isOptionalReceiver + ? 'OptionalCallExpression' + : 'CallExpression'; + return transformer.createNode< + babel.CallExpression | babel.OptionalCallExpression + >({ + type: nodeType, + callee, + arguments: arguments_, + ...(nodeType === 'OptionalCallExpression' ? { optional } : undefined), + }); + }; + +export const visitCall = transformCall(callOptions); +export const visitSafeCall = transformCall(safeCallOptions); diff --git a/src/transforms/transform-member-expression.ts b/src/transforms/transform-member-expression.ts new file mode 100644 index 00000000..0143bb97 --- /dev/null +++ b/src/transforms/transform-member-expression.ts @@ -0,0 +1,101 @@ +import { + type KeyedRead, + type PropertyRead, + type SafeKeyedRead, + type SafePropertyRead, +} from '@angular/compiler'; +import type * as babel from '@babel/types'; + +import { type Transformer } from '../transform-ast.ts'; +import { isOptionalObjectOrCallee } from './utilities.ts'; + +const keyedReadOptions = { computed: true, optional: false } as const; +const safeKeyedReadOptions = { computed: true, optional: true } as const; +const propertyReadOptions = { computed: false, optional: false } as const; +const safePropertyReadOptions = { computed: false, optional: true } as const; + +type VisitorKeyedRead = { + node: KeyedRead; + options: typeof keyedReadOptions; +}; +type VisitorSafeKeyedRead = { + node: SafeKeyedRead; + options: typeof safeKeyedReadOptions; +}; +type VisitorPropertyRead = { + node: PropertyRead; + options: typeof propertyReadOptions; +}; +type VisitorSafePropertyRead = { + node: SafePropertyRead; + options: typeof safePropertyReadOptions; +}; + +const transformMemberExpression = + < + Visitor extends + | VisitorKeyedRead + | VisitorSafeKeyedRead + | VisitorPropertyRead + | VisitorSafePropertyRead, + >({ + computed, + optional, + }: Visitor['options']) => + (node: Visitor['node'], transformer: Transformer) => { + const object = transformer.transformChild(node.receiver); + + let property; + if (computed) { + const { key } = node as KeyedRead | SafeKeyedRead; + property = transformer.transformChild(key); + } else { + const { name, nameSpan } = node as PropertyRead | SafePropertyRead; + property = transformer.createNode( + { type: 'Identifier', name: name }, + nameSpan, + object ? [] : transformer.ancestors, + ); + } + + if (!object) { + return property; + } + + const isOptionalObject = isOptionalObjectOrCallee(object); + + if (optional || isOptionalObject) { + return transformer.createNode({ + type: 'OptionalMemberExpression', + optional: optional || !isOptionalObject, + computed, + property, + object, + }); + } + + if (computed) { + return transformer.createNode({ + type: 'MemberExpression', + property, + object, + computed: true, + }); + } + + return transformer.createNode({ + type: 'MemberExpression', + object, + property: property as babel.MemberExpressionNonComputed['property'], + computed: false, + }); + }; + +export const visitKeyedRead = + transformMemberExpression(keyedReadOptions); +export const visitSafeKeyedRead = + transformMemberExpression(safeKeyedReadOptions); +export const visitPropertyRead = + transformMemberExpression(propertyReadOptions); +export const visitSafePropertyRead = + transformMemberExpression(safePropertyReadOptions); diff --git a/src/transforms/transform-unary-expression.ts b/src/transforms/transform-unary-expression.ts new file mode 100644 index 00000000..58e85745 --- /dev/null +++ b/src/transforms/transform-unary-expression.ts @@ -0,0 +1,55 @@ +import { + type PrefixNot, + type TypeofExpression, + type Unary, + type VoidExpression, +} from '@angular/compiler'; +import type * as babel from '@babel/types'; + +import { type Transformer } from '../transform-ast.ts'; + +type VisitorPrefixNot = { + node: PrefixNot; + operator: '!'; +}; +type VisitorTypeofExpression = { + node: TypeofExpression; + operator: 'typeof'; +}; +type VisitorVoidExpression = { + node: VoidExpression; + operator: 'void'; +}; + +const transformUnaryExpression = + < + Visitor extends + | VisitorPrefixNot + | VisitorTypeofExpression + | VisitorVoidExpression, + >( + operator: Visitor['operator'], + ) => + (node: Visitor['node'], transformer: Transformer) => + transformer.createNode( + { + type: 'UnaryExpression', + prefix: true, + operator, + argument: transformer.transformChild(node.expression), + }, + node.sourceSpan, + ); + +export const visitPrefixNot = transformUnaryExpression('!'); +export const visitTypeofExpression = + transformUnaryExpression('typeof'); +export const visitVoidExpression = + transformUnaryExpression('void'); +export const visitUnary = (node: Unary, transformer: Transformer) => + transformer.createNode({ + type: 'UnaryExpression', + prefix: true, + argument: transformer.transformChild(node.expr), + operator: node.operator as '-' | '+', + }); diff --git a/src/transforms/utilities.ts b/src/transforms/utilities.ts new file mode 100644 index 00000000..454438dc --- /dev/null +++ b/src/transforms/utilities.ts @@ -0,0 +1,17 @@ +import type { NGNode } from '../types.ts'; + +function isParenthesized(node: NGNode) { + return Boolean(node.extra?.parenthesized); +} + +export function isOptionalObjectOrCallee(node: NGNode): boolean { + if (node.type === 'TSNonNullExpression' && !isParenthesized(node)) { + return isOptionalObjectOrCallee(node.expression); + } + + return ( + (node.type === 'OptionalCallExpression' || + node.type === 'OptionalMemberExpression') && + !isParenthesized(node) + ); +} From 1f4d75b38552ea0b1cdab69e1f03acddbd3df628 Mon Sep 17 00:00:00 2001 From: fisker Date: Thu, 8 Jan 2026 21:45:34 +0800 Subject: [PATCH 04/16] Refactor --- src/transform-ast.ts | 2 +- src/transforms/transform-literal.ts | 55 +++++++++ src/transforms/transform-object-expression.ts | 44 +++++++ .../visitor.ts} | 113 +++--------------- 4 files changed, 115 insertions(+), 99 deletions(-) create mode 100644 src/transforms/transform-literal.ts create mode 100644 src/transforms/transform-object-expression.ts rename src/{transform-visitor.ts => transforms/visitor.ts} (64%) diff --git a/src/transform-ast.ts b/src/transform-ast.ts index f48a84a6..d998c834 100644 --- a/src/transform-ast.ts +++ b/src/transform-ast.ts @@ -1,7 +1,7 @@ import * as angular from '@angular/compiler'; import { Source } from './source.ts'; -import { transformVisitor } from './transform-visitor.ts'; +import { transformVisitor } from './transforms/visitor.ts'; import type { NGEmptyExpression, NGNode, RawNGSpan } from './types.ts'; class Transformer extends Source { diff --git a/src/transforms/transform-literal.ts b/src/transforms/transform-literal.ts new file mode 100644 index 00000000..ffc13acf --- /dev/null +++ b/src/transforms/transform-literal.ts @@ -0,0 +1,55 @@ +import { + type LiteralPrimitive, + type RegularExpressionLiteral, +} from '@angular/compiler'; +import type * as babel from '@babel/types'; + +import { type Transformer } from '../transform-ast.ts'; + +export const visitLiteralPrimitive = ( + node: LiteralPrimitive, + transformer: Transformer, +) => { + const { value } = node; + switch (typeof value) { + case 'boolean': + return transformer.createNode({ + type: 'BooleanLiteral', + value, + }); + case 'number': + return transformer.createNode({ + type: 'NumericLiteral', + value, + }); + case 'object': + return transformer.createNode({ + type: 'NullLiteral', + }); + case 'string': + return transformer.createNode({ + type: 'StringLiteral', + value, + }); + case 'undefined': + return transformer.createNode({ + type: 'Identifier', + name: 'undefined', + }); + /* c8 ignore next 4 */ + default: + throw new Error( + `Unexpected 'LiteralPrimitive' value type ${typeof value}`, + ); + } +}; + +export const visitRegularExpressionLiteral = ( + node: RegularExpressionLiteral, + transformer: Transformer, +) => + transformer.createNode({ + type: 'RegExpLiteral', + pattern: node.body, + flags: node.flags ?? '', + }); diff --git a/src/transforms/transform-object-expression.ts b/src/transforms/transform-object-expression.ts new file mode 100644 index 00000000..5bfba7d7 --- /dev/null +++ b/src/transforms/transform-object-expression.ts @@ -0,0 +1,44 @@ +import type * as angular from '@angular/compiler'; +import type * as babel from '@babel/types'; + +import { type Transformer } from '../transform-ast.ts'; +import type { NGNode, RawNGSpan } from '../types.ts'; + +export const visitLiteralMap = ( + node: angular.LiteralMap, + transformer: Transformer, +) => { + const { keys, values } = node; + const createChild = ( + properties: Partial & { type: T['type'] }, + location: angular.AST | RawNGSpan | [number, number] = node, + ) => + transformer.create(properties, location, [node, ...transformer.ancestors]); + + return transformer.createNode({ + type: 'ObjectExpression', + properties: keys.map((keyNode, index) => { + const valueNode = values[index]; + const shorthand = Boolean(keyNode.isShorthandInitialized); + const key = createChild( + keyNode.quoted + ? { type: 'StringLiteral', value: keyNode.key } + : { type: 'Identifier', name: keyNode.key }, + keyNode.sourceSpan, + ); + + return createChild( + { + type: 'ObjectProperty', + key, + value: transformer.transformChild(valueNode), + shorthand, + computed: false, + // @ts-expect-error -- Missed in types + method: false, + }, + [keyNode.sourceSpan.start, valueNode.sourceSpan.end], + ); + }), + }); +}; diff --git a/src/transform-visitor.ts b/src/transforms/visitor.ts similarity index 64% rename from src/transform-visitor.ts rename to src/transforms/visitor.ts index 588bcf8a..b0427154 100644 --- a/src/transform-visitor.ts +++ b/src/transforms/visitor.ts @@ -1,26 +1,26 @@ import * as angular from '@angular/compiler'; import type * as babel from '@babel/types'; -import { type Transformer } from './transform-ast.ts'; -import { visitCall, visitSafeCall } from './transforms/transform-call.ts'; +import { type Transformer } from '../transform-ast.ts'; +import type { NGChainedExpression, NGPipeExpression } from '../types.ts'; +import { visitCall, visitSafeCall } from './transform-call.ts'; +import { + visitLiteralPrimitive, + visitRegularExpressionLiteral, +} from './transform-literal.ts'; import { visitKeyedRead, visitPropertyRead, visitSafeKeyedRead, visitSafePropertyRead, -} from './transforms/transform-member-expression.ts'; +} from './transform-member-expression.ts'; +import { visitLiteralMap } from './transform-object-expression.ts'; import { visitPrefixNot, visitTypeofExpression, visitUnary, visitVoidExpression, -} from './transforms/transform-unary-expression.ts'; -import type { - NGChainedExpression, - NGNode, - NGPipeExpression, - RawNGSpan, -} from './types.ts'; +} from './transform-unary-expression.ts'; type AstVisitor = Required< Omit @@ -40,6 +40,11 @@ export const transformVisitor: AstVisitor = { visitVoidExpression, visitUnary, + visitLiteralMap, + + visitLiteralPrimitive, + visitRegularExpressionLiteral, + visitBinary(node: angular.Binary, transformer: Transformer) { const { operation: operator } = node; const [left, right] = transformer.transformChildren([ @@ -125,94 +130,6 @@ export const transformVisitor: AstVisitor = { }); }, - visitLiteralMap(node: angular.LiteralMap, transformer: Transformer) { - const { keys, values } = node; - const createChild = ( - properties: Partial & { type: T['type'] }, - location: angular.AST | RawNGSpan | [number, number] = node, - ) => - transformer.create(properties, location, [ - node, - ...transformer.ancestors, - ]); - - return transformer.createNode({ - type: 'ObjectExpression', - properties: keys.map((keyNode, index) => { - const valueNode = values[index]; - const shorthand = Boolean(keyNode.isShorthandInitialized); - const key = createChild( - keyNode.quoted - ? { type: 'StringLiteral', value: keyNode.key } - : { type: 'Identifier', name: keyNode.key }, - keyNode.sourceSpan, - ); - - return createChild( - { - type: 'ObjectProperty', - key, - value: transformer.transformChild(valueNode), - shorthand, - computed: false, - // @ts-expect-error -- Missed in types - method: false, - }, - [keyNode.sourceSpan.start, valueNode.sourceSpan.end], - ); - }), - }); - }, - - visitLiteralPrimitive( - node: angular.LiteralPrimitive, - transformer: Transformer, - ) { - const { value } = node; - switch (typeof value) { - case 'boolean': - return transformer.createNode({ - type: 'BooleanLiteral', - value, - }); - case 'number': - return transformer.createNode({ - type: 'NumericLiteral', - value, - }); - case 'object': - return transformer.createNode({ - type: 'NullLiteral', - }); - case 'string': - return transformer.createNode({ - type: 'StringLiteral', - value, - }); - case 'undefined': - return transformer.createNode({ - type: 'Identifier', - name: 'undefined', - }); - /* c8 ignore next 4 */ - default: - throw new Error( - `Unexpected LiteralPrimitive value type ${typeof value}`, - ); - } - }, - - visitRegularExpressionLiteral( - node: angular.RegularExpressionLiteral, - transformer: Transformer, - ) { - return transformer.createNode({ - type: 'RegExpLiteral', - pattern: node.body, - flags: node.flags ?? '', - }); - }, - visitNonNullAssert(node: angular.NonNullAssert, transformer: Transformer) { return transformer.createNode({ type: 'TSNonNullExpression', From 96739262faa3494aca2ecf3c4aff9bfab9fe2d10 Mon Sep 17 00:00:00 2001 From: fisker Date: Thu, 8 Jan 2026 21:59:25 +0800 Subject: [PATCH 05/16] Refactor --- src/transforms/transform-binary-expression.ts | 47 ++++++++++ ...m-call.ts => transform-call-expression.ts} | 0 src/transforms/transform-template-literal.ts | 52 +++++++++++ src/transforms/visitor.ts | 93 +++---------------- 4 files changed, 112 insertions(+), 80 deletions(-) create mode 100644 src/transforms/transform-binary-expression.ts rename src/transforms/{transform-call.ts => transform-call-expression.ts} (100%) create mode 100644 src/transforms/transform-template-literal.ts diff --git a/src/transforms/transform-binary-expression.ts b/src/transforms/transform-binary-expression.ts new file mode 100644 index 00000000..e4bb43b4 --- /dev/null +++ b/src/transforms/transform-binary-expression.ts @@ -0,0 +1,47 @@ +import { Binary } from '@angular/compiler'; +import type * as babel from '@babel/types'; + +import { type Transformer } from '../transform-ast.ts'; + +const isAssignmentOperator = ( + operator: Binary['operation'], +): operator is babel.AssignmentExpression['operator'] => + Binary.isAssignmentOperation(operator); + +const isLogicalOperator = ( + operator: Binary['operation'], +): operator is babel.LogicalExpression['operator'] => + operator === '&&' || operator === '||' || operator === '??'; + +export const visitBinary = (node: Binary, transformer: Transformer) => { + const { operation: operator } = node; + const [left, right] = transformer.transformChildren([ + node.left, + node.right, + ]); + + if (isLogicalOperator(operator)) { + return transformer.createNode({ + type: 'LogicalExpression', + operator, + left, + right, + }); + } + + if (isAssignmentOperator(operator)) { + return transformer.createNode({ + type: 'AssignmentExpression', + left: left as babel.MemberExpression, + right, + operator: operator, + }); + } + + return transformer.createNode({ + left, + right, + type: 'BinaryExpression', + operator: operator as babel.BinaryExpression['operator'], + }); +}; diff --git a/src/transforms/transform-call.ts b/src/transforms/transform-call-expression.ts similarity index 100% rename from src/transforms/transform-call.ts rename to src/transforms/transform-call-expression.ts diff --git a/src/transforms/transform-template-literal.ts b/src/transforms/transform-template-literal.ts new file mode 100644 index 00000000..527c4314 --- /dev/null +++ b/src/transforms/transform-template-literal.ts @@ -0,0 +1,52 @@ +import { + type TaggedTemplateLiteral, + type TemplateLiteral, + type TemplateLiteralElement, +} from '@angular/compiler'; +import type * as babel from '@babel/types'; + +import { type Transformer } from '../transform-ast.ts'; + +export const visitTaggedTemplateLiteral = ( + node: TaggedTemplateLiteral, + transformer: Transformer, +) => + transformer.createNode({ + type: 'TaggedTemplateExpression', + tag: transformer.transformChild(node.tag), + quasi: transformer.transformChild(node.template), + }); + +export const visitTemplateLiteral = ( + node: TemplateLiteral, + transformer: Transformer, +) => + transformer.createNode({ + type: 'TemplateLiteral', + quasis: transformer.transformChildren(node.elements), + expressions: transformer.transformChildren(node.expressions), + }); + +export const visitTemplateLiteralElement = ( + node: TemplateLiteralElement, + transformer: Transformer, +) => { + const [parent] = transformer.ancestors; + const { elements } = parent as TemplateLiteral; + const elementIndex = elements.indexOf(node); + const isFirst = elementIndex === 0; + const isLast = elementIndex === elements.length - 1; + + const end = node.sourceSpan.end - (isLast ? 1 : 0); + const start = node.sourceSpan.start + (isFirst ? 1 : 0); + const raw = transformer.text.slice(start, end); + + return transformer.createNode( + { + type: 'TemplateElement', + value: { cooked: node.text, raw }, + tail: isLast, + }, + [start, end], + ); +}; diff --git a/src/transforms/visitor.ts b/src/transforms/visitor.ts index b0427154..7c13b964 100644 --- a/src/transforms/visitor.ts +++ b/src/transforms/visitor.ts @@ -1,9 +1,10 @@ -import * as angular from '@angular/compiler'; +import type * as angular from '@angular/compiler'; import type * as babel from '@babel/types'; import { type Transformer } from '../transform-ast.ts'; import type { NGChainedExpression, NGPipeExpression } from '../types.ts'; -import { visitCall, visitSafeCall } from './transform-call.ts'; +import { visitBinary } from './transform-binary-expression.ts'; +import { visitCall, visitSafeCall } from './transform-call-expression.ts'; import { visitLiteralPrimitive, visitRegularExpressionLiteral, @@ -15,6 +16,11 @@ import { visitSafePropertyRead, } from './transform-member-expression.ts'; import { visitLiteralMap } from './transform-object-expression.ts'; +import { + visitTaggedTemplateLiteral, + visitTemplateLiteral, + visitTemplateLiteralElement, +} from './transform-template-literal.ts'; import { visitPrefixNot, visitTypeofExpression, @@ -40,43 +46,16 @@ export const transformVisitor: AstVisitor = { visitVoidExpression, visitUnary, + visitBinary, + visitLiteralMap, visitLiteralPrimitive, visitRegularExpressionLiteral, - visitBinary(node: angular.Binary, transformer: Transformer) { - const { operation: operator } = node; - const [left, right] = transformer.transformChildren([ - node.left, - node.right, - ]); - - if (operator === '&&' || operator === '||' || operator === '??') { - return transformer.createNode({ - type: 'LogicalExpression', - operator: operator as babel.LogicalExpression['operator'], - left, - right, - }); - } - - if (angular.Binary.isAssignmentOperation(operator)) { - return transformer.createNode({ - type: 'AssignmentExpression', - left: left as babel.MemberExpression, - right, - operator: operator as babel.AssignmentExpression['operator'], - }); - } - - return transformer.createNode({ - left, - right, - type: 'BinaryExpression', - operator: operator as babel.BinaryExpression['operator'], - }); - }, + visitTaggedTemplateLiteral, + visitTemplateLiteral, + visitTemplateLiteralElement, visitPipe(node: angular.BindingPipe, transformer: Transformer) { return transformer.createNode({ @@ -137,52 +116,6 @@ export const transformVisitor: AstVisitor = { }); }, - visitTaggedTemplateLiteral( - node: angular.TaggedTemplateLiteral, - transformer: Transformer, - ) { - return transformer.createNode({ - type: 'TaggedTemplateExpression', - tag: transformer.transformChild(node.tag), - quasi: transformer.transformChild(node.template), - }); - }, - - visitTemplateLiteral( - node: angular.TemplateLiteral, - transformer: Transformer, - ) { - return transformer.createNode({ - type: 'TemplateLiteral', - quasis: transformer.transformChildren(node.elements), - expressions: transformer.transformChildren(node.expressions), - }); - }, - - visitTemplateLiteralElement( - node: angular.TemplateLiteralElement, - transformer: Transformer, - ) { - const [parent] = transformer.ancestors; - const { elements } = parent as angular.TemplateLiteral; - const elementIndex = elements.indexOf(node); - const isFirst = elementIndex === 0; - const isLast = elementIndex === elements.length - 1; - - const end = node.sourceSpan.end - (isLast ? 1 : 0); - const start = node.sourceSpan.start + (isFirst ? 1 : 0); - const raw = transformer.text.slice(start, end); - - return transformer.createNode( - { - type: 'TemplateElement', - value: { cooked: node.text, raw }, - tail: isLast, - }, - [start, end], - ); - }, - visitParenthesizedExpression( node: angular.ParenthesizedExpression, transformer: Transformer, From b5f456f90f9a74ff71577710af010823a5e7fbe9 Mon Sep 17 00:00:00 2001 From: fisker Date: Thu, 8 Jan 2026 22:05:48 +0800 Subject: [PATCH 06/16] Extract code --- .../transform-conditional-expression.ts | 23 +++++++++++++++++++ src/transforms/transform-literal.ts | 1 + src/transforms/visitor.ts | 19 +++------------ 3 files changed, 27 insertions(+), 16 deletions(-) create mode 100644 src/transforms/transform-conditional-expression.ts diff --git a/src/transforms/transform-conditional-expression.ts b/src/transforms/transform-conditional-expression.ts new file mode 100644 index 00000000..41f31e82 --- /dev/null +++ b/src/transforms/transform-conditional-expression.ts @@ -0,0 +1,23 @@ +import { type Conditional } from '@angular/compiler'; +import type * as babel from '@babel/types'; + +import { type Transformer } from '../transform-ast.ts'; + +export const visitConditional = ( + node: Conditional, + transformer: Transformer, +) => { + const [test, consequent, alternate] = + transformer.transformChildren([ + node.condition, + node.trueExp, + node.falseExp, + ]); + + return transformer.createNode({ + type: 'ConditionalExpression', + test, + consequent, + alternate, + }); +}; diff --git a/src/transforms/transform-literal.ts b/src/transforms/transform-literal.ts index ffc13acf..d1100bfb 100644 --- a/src/transforms/transform-literal.ts +++ b/src/transforms/transform-literal.ts @@ -11,6 +11,7 @@ export const visitLiteralPrimitive = ( transformer: Transformer, ) => { const { value } = node; + console.log(node); switch (typeof value) { case 'boolean': return transformer.createNode({ diff --git a/src/transforms/visitor.ts b/src/transforms/visitor.ts index 7c13b964..e8e599cf 100644 --- a/src/transforms/visitor.ts +++ b/src/transforms/visitor.ts @@ -5,6 +5,7 @@ import { type Transformer } from '../transform-ast.ts'; import type { NGChainedExpression, NGPipeExpression } from '../types.ts'; import { visitBinary } from './transform-binary-expression.ts'; import { visitCall, visitSafeCall } from './transform-call-expression.ts'; +import { visitConditional } from './transform-conditional-expression.ts'; import { visitLiteralPrimitive, visitRegularExpressionLiteral, @@ -57,6 +58,8 @@ export const transformVisitor: AstVisitor = { visitTemplateLiteral, visitTemplateLiteralElement, + visitConditional, + visitPipe(node: angular.BindingPipe, transformer: Transformer) { return transformer.createNode({ type: 'NGPipeExpression', @@ -78,22 +81,6 @@ export const transformVisitor: AstVisitor = { }); }, - visitConditional(node: angular.Conditional, transformer: Transformer) { - const [test, consequent, alternate] = - transformer.transformChildren([ - node.condition, - node.trueExp, - node.falseExp, - ]); - - return transformer.createNode({ - type: 'ConditionalExpression', - test, - consequent, - alternate, - }); - }, - visitThisReceiver(node: angular.ThisReceiver, transformer: Transformer) { return transformer.createNode({ type: 'ThisExpression', From 379fd2a4e0c190a81d369a7060a31adf85ada92a Mon Sep 17 00:00:00 2001 From: fisker Date: Thu, 8 Jan 2026 22:45:14 +0800 Subject: [PATCH 07/16] Simplify --- src/transform-ast.ts | 13 +++- src/transforms/transform-binary-expression.ts | 23 +++---- src/transforms/transform-call-expression.ts | 29 ++++----- .../transform-conditional-expression.ts | 6 +- src/transforms/transform-literal.ts | 47 +++++--------- src/transforms/transform-member-expression.ts | 37 ++++++----- src/transforms/transform-object-expression.ts | 6 +- src/transforms/transform-template-literal.ts | 14 ++--- src/transforms/transform-unary-expression.ts | 32 +++++----- src/transforms/transform-unexpected-node.ts | 12 ++++ src/transforms/visitor.ts | 63 ++++++++++--------- 11 files changed, 149 insertions(+), 133 deletions(-) create mode 100644 src/transforms/transform-unexpected-node.ts diff --git a/src/transform-ast.ts b/src/transform-ast.ts index d998c834..0614c1d1 100644 --- a/src/transform-ast.ts +++ b/src/transform-ast.ts @@ -68,7 +68,18 @@ class Transformer extends Source { ) as T; } - return node.visit(transformVisitor, this) as T; + const properties = node.visit(transformVisitor, this); + + if (properties.range) { + properties.start ??= properties.range[0]; + properties.end ??= properties.range[1]; + return properties as T; + } + + const { location = node.sourceSpan, ...restProperties } = properties; + const estreeNode = this.createNode(restProperties, location); + + return estreeNode as T; } static transform(node: angular.AST, text: string) { diff --git a/src/transforms/transform-binary-expression.ts b/src/transforms/transform-binary-expression.ts index e4bb43b4..f2ccffcc 100644 --- a/src/transforms/transform-binary-expression.ts +++ b/src/transforms/transform-binary-expression.ts @@ -13,7 +13,13 @@ const isLogicalOperator = ( ): operator is babel.LogicalExpression['operator'] => operator === '&&' || operator === '||' || operator === '??'; -export const visitBinary = (node: Binary, transformer: Transformer) => { +export const visitBinary = ( + node: Binary, + transformer: Transformer, +): + | babel.LogicalExpression + | babel.AssignmentExpression + | babel.BinaryExpression => { const { operation: operator } = node; const [left, right] = transformer.transformChildren([ node.left, @@ -21,27 +27,22 @@ export const visitBinary = (node: Binary, transformer: Transformer) => { ]); if (isLogicalOperator(operator)) { - return transformer.createNode({ - type: 'LogicalExpression', - operator, - left, - right, - }); + return { type: 'LogicalExpression', operator, left, right }; } if (isAssignmentOperator(operator)) { - return transformer.createNode({ + return { type: 'AssignmentExpression', left: left as babel.MemberExpression, right, operator: operator, - }); + }; } - return transformer.createNode({ + return { left, right, type: 'BinaryExpression', operator: operator as babel.BinaryExpression['operator'], - }); + }; }; diff --git a/src/transforms/transform-call-expression.ts b/src/transforms/transform-call-expression.ts index ef835378..313d5205 100644 --- a/src/transforms/transform-call-expression.ts +++ b/src/transforms/transform-call-expression.ts @@ -20,24 +20,25 @@ const transformCall = ({ optional, }: Visitor['options']) => - (node: Visitor['node'], transformer: Transformer) => { + ( + node: Visitor['node'], + transformer: Transformer, + ): babel.CallExpression | babel.OptionalCallExpression => { const arguments_ = transformer.transformChildren( node.args, ); const callee = transformer.transformChild(node.receiver); - const isOptionalReceiver = isOptionalObjectOrCallee(callee); - const nodeType = - optional || isOptionalReceiver - ? 'OptionalCallExpression' - : 'CallExpression'; - return transformer.createNode< - babel.CallExpression | babel.OptionalCallExpression - >({ - type: nodeType, - callee, - arguments: arguments_, - ...(nodeType === 'OptionalCallExpression' ? { optional } : undefined), - }); + + if (optional || isOptionalObjectOrCallee(callee)) { + return { + type: 'OptionalCallExpression', + callee, + arguments: arguments_, + optional, + }; + } + + return { type: 'CallExpression', callee, arguments: arguments_ }; }; export const visitCall = transformCall(callOptions); diff --git a/src/transforms/transform-conditional-expression.ts b/src/transforms/transform-conditional-expression.ts index 41f31e82..c4afca14 100644 --- a/src/transforms/transform-conditional-expression.ts +++ b/src/transforms/transform-conditional-expression.ts @@ -6,7 +6,7 @@ import { type Transformer } from '../transform-ast.ts'; export const visitConditional = ( node: Conditional, transformer: Transformer, -) => { +): babel.ConditionalExpression => { const [test, consequent, alternate] = transformer.transformChildren([ node.condition, @@ -14,10 +14,10 @@ export const visitConditional = ( node.falseExp, ]); - return transformer.createNode({ + return { type: 'ConditionalExpression', test, consequent, alternate, - }); + }; }; diff --git a/src/transforms/transform-literal.ts b/src/transforms/transform-literal.ts index d1100bfb..e5817afa 100644 --- a/src/transforms/transform-literal.ts +++ b/src/transforms/transform-literal.ts @@ -4,39 +4,26 @@ import { } from '@angular/compiler'; import type * as babel from '@babel/types'; -import { type Transformer } from '../transform-ast.ts'; - export const visitLiteralPrimitive = ( node: LiteralPrimitive, - transformer: Transformer, -) => { +): + | babel.BooleanLiteral + | babel.NumericLiteral + | babel.NullLiteral + | babel.StringLiteral + | babel.Identifier => { const { value } = node; - console.log(node); switch (typeof value) { case 'boolean': - return transformer.createNode({ - type: 'BooleanLiteral', - value, - }); + return { type: 'BooleanLiteral', value }; case 'number': - return transformer.createNode({ - type: 'NumericLiteral', - value, - }); + return { type: 'NumericLiteral', value }; case 'object': - return transformer.createNode({ - type: 'NullLiteral', - }); + return { type: 'NullLiteral' }; case 'string': - return transformer.createNode({ - type: 'StringLiteral', - value, - }); + return { type: 'StringLiteral', value }; case 'undefined': - return transformer.createNode({ - type: 'Identifier', - name: 'undefined', - }); + return { type: 'Identifier', name: 'undefined' }; /* c8 ignore next 4 */ default: throw new Error( @@ -47,10 +34,8 @@ export const visitLiteralPrimitive = ( export const visitRegularExpressionLiteral = ( node: RegularExpressionLiteral, - transformer: Transformer, -) => - transformer.createNode({ - type: 'RegExpLiteral', - pattern: node.body, - flags: node.flags ?? '', - }); +): babel.RegExpLiteral => ({ + type: 'RegExpLiteral', + pattern: node.body, + flags: node.flags ?? '', +}); diff --git a/src/transforms/transform-member-expression.ts b/src/transforms/transform-member-expression.ts index 0143bb97..04a90c0b 100644 --- a/src/transforms/transform-member-expression.ts +++ b/src/transforms/transform-member-expression.ts @@ -1,4 +1,5 @@ import { + ImplicitReceiver, type KeyedRead, type PropertyRead, type SafeKeyedRead, @@ -42,53 +43,57 @@ const transformMemberExpression = computed, optional, }: Visitor['options']) => - (node: Visitor['node'], transformer: Transformer) => { - const object = transformer.transformChild(node.receiver); + ( + node: Visitor['node'], + transformer: Transformer, + ): + | babel.OptionalMemberExpression + | babel.MemberExpression + | babel.Identifier => { + const { receiver } = node; let property; if (computed) { const { key } = node as KeyedRead | SafeKeyedRead; property = transformer.transformChild(key); } else { + const isImplicitReceiver = receiver instanceof ImplicitReceiver; const { name, nameSpan } = node as PropertyRead | SafePropertyRead; property = transformer.createNode( { type: 'Identifier', name: name }, nameSpan, - object ? [] : transformer.ancestors, + isImplicitReceiver ? transformer.ancestors : [], ); - } - if (!object) { - return property; + if (isImplicitReceiver) { + return property; + } } + const object = transformer.transformChild(receiver); + const isOptionalObject = isOptionalObjectOrCallee(object); if (optional || isOptionalObject) { - return transformer.createNode({ + return { type: 'OptionalMemberExpression', optional: optional || !isOptionalObject, computed, property, object, - }); + }; } if (computed) { - return transformer.createNode({ - type: 'MemberExpression', - property, - object, - computed: true, - }); + return { type: 'MemberExpression', property, object, computed: true }; } - return transformer.createNode({ + return { type: 'MemberExpression', object, property: property as babel.MemberExpressionNonComputed['property'], computed: false, - }); + }; }; export const visitKeyedRead = diff --git a/src/transforms/transform-object-expression.ts b/src/transforms/transform-object-expression.ts index 5bfba7d7..e913daf8 100644 --- a/src/transforms/transform-object-expression.ts +++ b/src/transforms/transform-object-expression.ts @@ -7,7 +7,7 @@ import type { NGNode, RawNGSpan } from '../types.ts'; export const visitLiteralMap = ( node: angular.LiteralMap, transformer: Transformer, -) => { +): babel.ObjectExpression => { const { keys, values } = node; const createChild = ( properties: Partial & { type: T['type'] }, @@ -15,7 +15,7 @@ export const visitLiteralMap = ( ) => transformer.create(properties, location, [node, ...transformer.ancestors]); - return transformer.createNode({ + return { type: 'ObjectExpression', properties: keys.map((keyNode, index) => { const valueNode = values[index]; @@ -40,5 +40,5 @@ export const visitLiteralMap = ( [keyNode.sourceSpan.start, valueNode.sourceSpan.end], ); }), - }); + }; }; diff --git a/src/transforms/transform-template-literal.ts b/src/transforms/transform-template-literal.ts index 527c4314..c16e4be0 100644 --- a/src/transforms/transform-template-literal.ts +++ b/src/transforms/transform-template-literal.ts @@ -41,12 +41,10 @@ export const visitTemplateLiteralElement = ( const start = node.sourceSpan.start + (isFirst ? 1 : 0); const raw = transformer.text.slice(start, end); - return transformer.createNode( - { - type: 'TemplateElement', - value: { cooked: node.text, raw }, - tail: isLast, - }, - [start, end], - ); + return { + type: 'TemplateElement', + value: { cooked: node.text, raw }, + tail: isLast, + range: [start, end], + }; }; diff --git a/src/transforms/transform-unary-expression.ts b/src/transforms/transform-unary-expression.ts index 58e85745..0c6c3774 100644 --- a/src/transforms/transform-unary-expression.ts +++ b/src/transforms/transform-unary-expression.ts @@ -30,26 +30,24 @@ const transformUnaryExpression = >( operator: Visitor['operator'], ) => - (node: Visitor['node'], transformer: Transformer) => - transformer.createNode( - { - type: 'UnaryExpression', - prefix: true, - operator, - argument: transformer.transformChild(node.expression), - }, - node.sourceSpan, - ); + (node: Visitor['node'], transformer: Transformer): babel.UnaryExpression => ({ + type: 'UnaryExpression', + prefix: true, + operator, + argument: transformer.transformChild(node.expression), + }); export const visitPrefixNot = transformUnaryExpression('!'); export const visitTypeofExpression = transformUnaryExpression('typeof'); export const visitVoidExpression = transformUnaryExpression('void'); -export const visitUnary = (node: Unary, transformer: Transformer) => - transformer.createNode({ - type: 'UnaryExpression', - prefix: true, - argument: transformer.transformChild(node.expr), - operator: node.operator as '-' | '+', - }); +export const visitUnary = ( + node: Unary, + transformer: Transformer, +): babel.UnaryExpression => ({ + type: 'UnaryExpression', + prefix: true, + argument: transformer.transformChild(node.expr), + operator: node.operator as '-' | '+', +}); diff --git a/src/transforms/transform-unexpected-node.ts b/src/transforms/transform-unexpected-node.ts new file mode 100644 index 00000000..d99dacf9 --- /dev/null +++ b/src/transforms/transform-unexpected-node.ts @@ -0,0 +1,12 @@ +import { type ASTWithSource, type ImplicitReceiver } from '@angular/compiler'; + +function transformUnexpectedNode( + node: T, +) { + throw new Error(`Unexpected node type '${node.constructor.name}'`); +} + +// Handled in `./transform-member-expression.ts` +export const visitImplicitReceiver = transformUnexpectedNode; +// Unreachable +export const visitASTWithSource = transformUnexpectedNode; diff --git a/src/transforms/visitor.ts b/src/transforms/visitor.ts index e8e599cf..81625288 100644 --- a/src/transforms/visitor.ts +++ b/src/transforms/visitor.ts @@ -28,10 +28,12 @@ import { visitUnary, visitVoidExpression, } from './transform-unary-expression.ts'; +import { + visitASTWithSource, + visitImplicitReceiver, +} from './transform-unexpected-node.ts'; -type AstVisitor = Required< - Omit ->; +type AstVisitor = Required>; export const transformVisitor: AstVisitor = { visitCall, @@ -60,8 +62,11 @@ export const transformVisitor: AstVisitor = { visitConditional, - visitPipe(node: angular.BindingPipe, transformer: Transformer) { - return transformer.createNode({ + visitPipe( + node: angular.BindingPipe, + transformer: Transformer, + ): Omit { + return { type: 'NGPipeExpression', left: transformer.transformChild(node.exp), right: transformer.createNode( @@ -69,39 +74,38 @@ export const transformVisitor: AstVisitor = { node.nameSpan, ), arguments: transformer.transformChildren(node.args), - }); + }; }, - visitChain(node: angular.Chain, transformer: Transformer) { - return transformer.createNode({ + visitChain( + node: angular.Chain, + transformer: Transformer, + ): Omit { + return { type: 'NGChainedExpression', expressions: transformer.transformChildren( node.expressions, ), - }); + }; }, - visitThisReceiver(node: angular.ThisReceiver, transformer: Transformer) { - return transformer.createNode({ - type: 'ThisExpression', - }); - }, + visitThisReceiver: (): babel.ThisExpression => ({ type: 'ThisExpression' }), - visitLiteralArray(node: angular.LiteralArray, transformer: Transformer) { - return transformer.createNode({ - type: 'ArrayExpression', - elements: transformer.transformChildren( - node.expressions, - ), - }); - }, + visitLiteralArray: ( + node: angular.LiteralArray, + transformer: Transformer, + ): babel.ArrayExpression => ({ + type: 'ArrayExpression', + elements: transformer.transformChildren(node.expressions), + }), - visitNonNullAssert(node: angular.NonNullAssert, transformer: Transformer) { - return transformer.createNode({ - type: 'TSNonNullExpression', - expression: transformer.transformChild(node.expression), - }); - }, + visitNonNullAssert: ( + node: angular.NonNullAssert, + transformer: Transformer, + ): babel.TSNonNullExpression => ({ + type: 'TSNonNullExpression', + expression: transformer.transformChild(node.expression), + }), visitParenthesizedExpression( node: angular.ParenthesizedExpression, @@ -121,5 +125,6 @@ export const transformVisitor: AstVisitor = { return transformer.transformChild(expressions[0]); }, - visitImplicitReceiver() {}, + visitASTWithSource, + visitImplicitReceiver, }; From 1d7646a04330d69dd2ef884c0e139bb9059d16f7 Mon Sep 17 00:00:00 2001 From: fisker Date: Thu, 8 Jan 2026 23:10:07 +0800 Subject: [PATCH 08/16] Rewrite --- src/transforms/transform-array-expression.ts | 12 ++ .../transform-chained-expression.ts | 15 ++ src/transforms/transform-interpolation.ts | 17 +++ .../transform-non-null-expression.ts | 12 ++ .../transform-parenthesized-expression.ts | 8 ++ src/transforms/transform-pipe-expression.ts | 18 +++ src/transforms/transform-this-expression.ts | 5 + src/transforms/transforms.ts | 16 +++ src/transforms/visitor.ts | 131 +----------------- 9 files changed, 107 insertions(+), 127 deletions(-) create mode 100644 src/transforms/transform-array-expression.ts create mode 100644 src/transforms/transform-chained-expression.ts create mode 100644 src/transforms/transform-interpolation.ts create mode 100644 src/transforms/transform-non-null-expression.ts create mode 100644 src/transforms/transform-parenthesized-expression.ts create mode 100644 src/transforms/transform-pipe-expression.ts create mode 100644 src/transforms/transform-this-expression.ts create mode 100644 src/transforms/transforms.ts diff --git a/src/transforms/transform-array-expression.ts b/src/transforms/transform-array-expression.ts new file mode 100644 index 00000000..bfe93f73 --- /dev/null +++ b/src/transforms/transform-array-expression.ts @@ -0,0 +1,12 @@ +import { type LiteralArray } from '@angular/compiler'; +import type * as babel from '@babel/types'; + +import { type Transformer } from '../transform-ast.ts'; + +export const visitLiteralArray = ( + node: LiteralArray, + transformer: Transformer, +): babel.ArrayExpression => ({ + type: 'ArrayExpression', + elements: transformer.transformChildren(node.expressions), +}); diff --git a/src/transforms/transform-chained-expression.ts b/src/transforms/transform-chained-expression.ts new file mode 100644 index 00000000..6c92d1d5 --- /dev/null +++ b/src/transforms/transform-chained-expression.ts @@ -0,0 +1,15 @@ +import { type Chain } from '@angular/compiler'; +import type * as babel from '@babel/types'; + +import { type Transformer } from '../transform-ast.ts'; +import type { NGChainedExpression } from '../types.ts'; + +export const visitChain = ( + node: Chain, + transformer: Transformer, +): Omit => ({ + type: 'NGChainedExpression', + expressions: transformer.transformChildren( + node.expressions, + ), +}); diff --git a/src/transforms/transform-interpolation.ts b/src/transforms/transform-interpolation.ts new file mode 100644 index 00000000..1119c748 --- /dev/null +++ b/src/transforms/transform-interpolation.ts @@ -0,0 +1,17 @@ +import { type Interpolation } from '@angular/compiler'; + +import { type Transformer } from '../transform-ast.ts'; + +export const visitInterpolation = ( + node: Interpolation, + transformer: Transformer, +) => { + const { expressions } = node; + + /* c8 ignore next 3 @preserve */ + if (expressions.length !== 1) { + throw new Error("Unexpected 'Interpolation'"); + } + + return transformer.transformChild(expressions[0]); +}; diff --git a/src/transforms/transform-non-null-expression.ts b/src/transforms/transform-non-null-expression.ts new file mode 100644 index 00000000..8484a19e --- /dev/null +++ b/src/transforms/transform-non-null-expression.ts @@ -0,0 +1,12 @@ +import { type NonNullAssert } from '@angular/compiler'; +import type * as babel from '@babel/types'; + +import { type Transformer } from '../transform-ast.ts'; + +export const visitNonNullAssert = ( + node: NonNullAssert, + transformer: Transformer, +): babel.TSNonNullExpression => ({ + type: 'TSNonNullExpression', + expression: transformer.transformChild(node.expression), +}); diff --git a/src/transforms/transform-parenthesized-expression.ts b/src/transforms/transform-parenthesized-expression.ts new file mode 100644 index 00000000..8b307ff0 --- /dev/null +++ b/src/transforms/transform-parenthesized-expression.ts @@ -0,0 +1,8 @@ +import { type ParenthesizedExpression } from '@angular/compiler'; + +import { type Transformer } from '../transform-ast.ts'; + +export const visitParenthesizedExpression = ( + node: ParenthesizedExpression, + transformer: Transformer, +) => transformer.transformChild(node.expression); diff --git a/src/transforms/transform-pipe-expression.ts b/src/transforms/transform-pipe-expression.ts new file mode 100644 index 00000000..25416603 --- /dev/null +++ b/src/transforms/transform-pipe-expression.ts @@ -0,0 +1,18 @@ +import { type BindingPipe } from '@angular/compiler'; +import type * as babel from '@babel/types'; + +import { type Transformer } from '../transform-ast.ts'; +import type { NGPipeExpression } from '../types.ts'; + +export const visitPipe = ( + node: BindingPipe, + transformer: Transformer, +): Omit => ({ + type: 'NGPipeExpression', + left: transformer.transformChild(node.exp), + right: transformer.createNode( + { type: 'Identifier', name: node.name }, + node.nameSpan, + ), + arguments: transformer.transformChildren(node.args), +}); diff --git a/src/transforms/transform-this-expression.ts b/src/transforms/transform-this-expression.ts new file mode 100644 index 00000000..fdc8ab47 --- /dev/null +++ b/src/transforms/transform-this-expression.ts @@ -0,0 +1,5 @@ +import type * as babel from '@babel/types'; + +export const visitThisReceiver = (): babel.ThisExpression => ({ + type: 'ThisExpression', +}); diff --git a/src/transforms/transforms.ts b/src/transforms/transforms.ts new file mode 100644 index 00000000..d3e376eb --- /dev/null +++ b/src/transforms/transforms.ts @@ -0,0 +1,16 @@ +export * from './transform-array-expression.ts'; +export * from './transform-binary-expression.ts'; +export * from './transform-call-expression.ts'; +export * from './transform-chained-expression.ts'; +export * from './transform-conditional-expression.ts'; +export * from './transform-interpolation.ts'; +export * from './transform-literal.ts'; +export * from './transform-member-expression.ts'; +export * from './transform-non-null-expression.ts'; +export * from './transform-object-expression.ts'; +export * from './transform-parenthesized-expression.ts'; +export * from './transform-pipe-expression.ts'; +export * from './transform-template-literal.ts'; +export * from './transform-this-expression.ts'; +export * from './transform-unary-expression.ts'; +export * from './transform-unexpected-node.ts'; diff --git a/src/transforms/visitor.ts b/src/transforms/visitor.ts index 81625288..949692bb 100644 --- a/src/transforms/visitor.ts +++ b/src/transforms/visitor.ts @@ -1,130 +1,7 @@ -import type * as angular from '@angular/compiler'; -import type * as babel from '@babel/types'; +import { type AstVisitor as AngularAstVisitor } from '@angular/compiler'; -import { type Transformer } from '../transform-ast.ts'; -import type { NGChainedExpression, NGPipeExpression } from '../types.ts'; -import { visitBinary } from './transform-binary-expression.ts'; -import { visitCall, visitSafeCall } from './transform-call-expression.ts'; -import { visitConditional } from './transform-conditional-expression.ts'; -import { - visitLiteralPrimitive, - visitRegularExpressionLiteral, -} from './transform-literal.ts'; -import { - visitKeyedRead, - visitPropertyRead, - visitSafeKeyedRead, - visitSafePropertyRead, -} from './transform-member-expression.ts'; -import { visitLiteralMap } from './transform-object-expression.ts'; -import { - visitTaggedTemplateLiteral, - visitTemplateLiteral, - visitTemplateLiteralElement, -} from './transform-template-literal.ts'; -import { - visitPrefixNot, - visitTypeofExpression, - visitUnary, - visitVoidExpression, -} from './transform-unary-expression.ts'; -import { - visitASTWithSource, - visitImplicitReceiver, -} from './transform-unexpected-node.ts'; +import * as transforms from './transforms.ts'; -type AstVisitor = Required>; +type AstVisitor = Required>; -export const transformVisitor: AstVisitor = { - visitCall, - visitSafeCall, - - visitKeyedRead, - visitPropertyRead, - visitSafeKeyedRead, - visitSafePropertyRead, - - visitPrefixNot, - visitTypeofExpression, - visitVoidExpression, - visitUnary, - - visitBinary, - - visitLiteralMap, - - visitLiteralPrimitive, - visitRegularExpressionLiteral, - - visitTaggedTemplateLiteral, - visitTemplateLiteral, - visitTemplateLiteralElement, - - visitConditional, - - visitPipe( - node: angular.BindingPipe, - transformer: Transformer, - ): Omit { - return { - type: 'NGPipeExpression', - left: transformer.transformChild(node.exp), - right: transformer.createNode( - { type: 'Identifier', name: node.name }, - node.nameSpan, - ), - arguments: transformer.transformChildren(node.args), - }; - }, - - visitChain( - node: angular.Chain, - transformer: Transformer, - ): Omit { - return { - type: 'NGChainedExpression', - expressions: transformer.transformChildren( - node.expressions, - ), - }; - }, - - visitThisReceiver: (): babel.ThisExpression => ({ type: 'ThisExpression' }), - - visitLiteralArray: ( - node: angular.LiteralArray, - transformer: Transformer, - ): babel.ArrayExpression => ({ - type: 'ArrayExpression', - elements: transformer.transformChildren(node.expressions), - }), - - visitNonNullAssert: ( - node: angular.NonNullAssert, - transformer: Transformer, - ): babel.TSNonNullExpression => ({ - type: 'TSNonNullExpression', - expression: transformer.transformChild(node.expression), - }), - - visitParenthesizedExpression( - node: angular.ParenthesizedExpression, - transformer: Transformer, - ) { - return transformer.transformChild(node.expression); - }, - - visitInterpolation(node: angular.Interpolation, transformer: Transformer) { - const { expressions } = node; - - /* c8 ignore next 3 @preserve */ - if (expressions.length !== 1) { - throw new Error("Unexpected 'Interpolation'"); - } - - return transformer.transformChild(expressions[0]); - }, - - visitASTWithSource, - visitImplicitReceiver, -}; +export const transformVisitor: AstVisitor = transforms; From ae5c39a3e8fe2882d0748c54e159c96c6d16d288 Mon Sep 17 00:00:00 2001 From: fisker Date: Thu, 8 Jan 2026 23:14:22 +0800 Subject: [PATCH 09/16] Rename --- src/{transforms => ast-transform}/transform-array-expression.ts | 0 .../transform-binary-expression.ts | 0 src/{transforms => ast-transform}/transform-call-expression.ts | 0 .../transform-chained-expression.ts | 0 .../transform-conditional-expression.ts | 0 src/{transforms => ast-transform}/transform-interpolation.ts | 0 src/{transforms => ast-transform}/transform-literal.ts | 0 .../transform-member-expression.ts | 0 .../transform-non-null-expression.ts | 0 .../transform-object-expression.ts | 0 .../transform-parenthesized-expression.ts | 0 src/{transforms => ast-transform}/transform-pipe-expression.ts | 0 src/{transforms => ast-transform}/transform-template-literal.ts | 0 src/{transforms => ast-transform}/transform-this-expression.ts | 0 src/{transforms => ast-transform}/transform-unary-expression.ts | 0 src/{transforms => ast-transform}/transform-unexpected-node.ts | 0 src/{transforms => ast-transform}/transforms.ts | 0 src/{transforms => ast-transform}/utilities.ts | 0 src/{transforms => ast-transform}/visitor.ts | 0 src/transform-ast.ts | 2 +- 20 files changed, 1 insertion(+), 1 deletion(-) rename src/{transforms => ast-transform}/transform-array-expression.ts (100%) rename src/{transforms => ast-transform}/transform-binary-expression.ts (100%) rename src/{transforms => ast-transform}/transform-call-expression.ts (100%) rename src/{transforms => ast-transform}/transform-chained-expression.ts (100%) rename src/{transforms => ast-transform}/transform-conditional-expression.ts (100%) rename src/{transforms => ast-transform}/transform-interpolation.ts (100%) rename src/{transforms => ast-transform}/transform-literal.ts (100%) rename src/{transforms => ast-transform}/transform-member-expression.ts (100%) rename src/{transforms => ast-transform}/transform-non-null-expression.ts (100%) rename src/{transforms => ast-transform}/transform-object-expression.ts (100%) rename src/{transforms => ast-transform}/transform-parenthesized-expression.ts (100%) rename src/{transforms => ast-transform}/transform-pipe-expression.ts (100%) rename src/{transforms => ast-transform}/transform-template-literal.ts (100%) rename src/{transforms => ast-transform}/transform-this-expression.ts (100%) rename src/{transforms => ast-transform}/transform-unary-expression.ts (100%) rename src/{transforms => ast-transform}/transform-unexpected-node.ts (100%) rename src/{transforms => ast-transform}/transforms.ts (100%) rename src/{transforms => ast-transform}/utilities.ts (100%) rename src/{transforms => ast-transform}/visitor.ts (100%) diff --git a/src/transforms/transform-array-expression.ts b/src/ast-transform/transform-array-expression.ts similarity index 100% rename from src/transforms/transform-array-expression.ts rename to src/ast-transform/transform-array-expression.ts diff --git a/src/transforms/transform-binary-expression.ts b/src/ast-transform/transform-binary-expression.ts similarity index 100% rename from src/transforms/transform-binary-expression.ts rename to src/ast-transform/transform-binary-expression.ts diff --git a/src/transforms/transform-call-expression.ts b/src/ast-transform/transform-call-expression.ts similarity index 100% rename from src/transforms/transform-call-expression.ts rename to src/ast-transform/transform-call-expression.ts diff --git a/src/transforms/transform-chained-expression.ts b/src/ast-transform/transform-chained-expression.ts similarity index 100% rename from src/transforms/transform-chained-expression.ts rename to src/ast-transform/transform-chained-expression.ts diff --git a/src/transforms/transform-conditional-expression.ts b/src/ast-transform/transform-conditional-expression.ts similarity index 100% rename from src/transforms/transform-conditional-expression.ts rename to src/ast-transform/transform-conditional-expression.ts diff --git a/src/transforms/transform-interpolation.ts b/src/ast-transform/transform-interpolation.ts similarity index 100% rename from src/transforms/transform-interpolation.ts rename to src/ast-transform/transform-interpolation.ts diff --git a/src/transforms/transform-literal.ts b/src/ast-transform/transform-literal.ts similarity index 100% rename from src/transforms/transform-literal.ts rename to src/ast-transform/transform-literal.ts diff --git a/src/transforms/transform-member-expression.ts b/src/ast-transform/transform-member-expression.ts similarity index 100% rename from src/transforms/transform-member-expression.ts rename to src/ast-transform/transform-member-expression.ts diff --git a/src/transforms/transform-non-null-expression.ts b/src/ast-transform/transform-non-null-expression.ts similarity index 100% rename from src/transforms/transform-non-null-expression.ts rename to src/ast-transform/transform-non-null-expression.ts diff --git a/src/transforms/transform-object-expression.ts b/src/ast-transform/transform-object-expression.ts similarity index 100% rename from src/transforms/transform-object-expression.ts rename to src/ast-transform/transform-object-expression.ts diff --git a/src/transforms/transform-parenthesized-expression.ts b/src/ast-transform/transform-parenthesized-expression.ts similarity index 100% rename from src/transforms/transform-parenthesized-expression.ts rename to src/ast-transform/transform-parenthesized-expression.ts diff --git a/src/transforms/transform-pipe-expression.ts b/src/ast-transform/transform-pipe-expression.ts similarity index 100% rename from src/transforms/transform-pipe-expression.ts rename to src/ast-transform/transform-pipe-expression.ts diff --git a/src/transforms/transform-template-literal.ts b/src/ast-transform/transform-template-literal.ts similarity index 100% rename from src/transforms/transform-template-literal.ts rename to src/ast-transform/transform-template-literal.ts diff --git a/src/transforms/transform-this-expression.ts b/src/ast-transform/transform-this-expression.ts similarity index 100% rename from src/transforms/transform-this-expression.ts rename to src/ast-transform/transform-this-expression.ts diff --git a/src/transforms/transform-unary-expression.ts b/src/ast-transform/transform-unary-expression.ts similarity index 100% rename from src/transforms/transform-unary-expression.ts rename to src/ast-transform/transform-unary-expression.ts diff --git a/src/transforms/transform-unexpected-node.ts b/src/ast-transform/transform-unexpected-node.ts similarity index 100% rename from src/transforms/transform-unexpected-node.ts rename to src/ast-transform/transform-unexpected-node.ts diff --git a/src/transforms/transforms.ts b/src/ast-transform/transforms.ts similarity index 100% rename from src/transforms/transforms.ts rename to src/ast-transform/transforms.ts diff --git a/src/transforms/utilities.ts b/src/ast-transform/utilities.ts similarity index 100% rename from src/transforms/utilities.ts rename to src/ast-transform/utilities.ts diff --git a/src/transforms/visitor.ts b/src/ast-transform/visitor.ts similarity index 100% rename from src/transforms/visitor.ts rename to src/ast-transform/visitor.ts diff --git a/src/transform-ast.ts b/src/transform-ast.ts index 0614c1d1..7f35187a 100644 --- a/src/transform-ast.ts +++ b/src/transform-ast.ts @@ -1,7 +1,7 @@ import * as angular from '@angular/compiler'; +import { transformVisitor } from './ast-transform/visitor.ts'; import { Source } from './source.ts'; -import { transformVisitor } from './transforms/visitor.ts'; import type { NGEmptyExpression, NGNode, RawNGSpan } from './types.ts'; class Transformer extends Source { From 0aed261385329c863e3280863358908d88137f51 Mon Sep 17 00:00:00 2001 From: fisker Date: Thu, 8 Jan 2026 23:19:13 +0800 Subject: [PATCH 10/16] Move files --- src/ast-transform/index.ts | 1 + src/ast-transform/transform-array-expression.ts | 2 +- src/ast-transform/transform-binary-expression.ts | 2 +- src/ast-transform/transform-call-expression.ts | 2 +- src/ast-transform/transform-chained-expression.ts | 2 +- src/ast-transform/transform-conditional-expression.ts | 2 +- src/ast-transform/transform-interpolation.ts | 2 +- src/ast-transform/transform-member-expression.ts | 2 +- src/ast-transform/transform-non-null-expression.ts | 2 +- src/ast-transform/transform-object-expression.ts | 2 +- src/ast-transform/transform-parenthesized-expression.ts | 2 +- src/ast-transform/transform-pipe-expression.ts | 2 +- src/ast-transform/transform-template-literal.ts | 2 +- src/ast-transform/transform-unary-expression.ts | 2 +- src/{transform-ast.ts => ast-transform/transform.ts} | 6 +++--- src/transform-template-binding.ts | 4 ++-- src/transform.ts | 4 ++-- 17 files changed, 21 insertions(+), 20 deletions(-) create mode 100644 src/ast-transform/index.ts rename src/{transform-ast.ts => ast-transform/transform.ts} (92%) diff --git a/src/ast-transform/index.ts b/src/ast-transform/index.ts new file mode 100644 index 00000000..ddffe4fd --- /dev/null +++ b/src/ast-transform/index.ts @@ -0,0 +1 @@ +export { transform } from './transform.ts'; diff --git a/src/ast-transform/transform-array-expression.ts b/src/ast-transform/transform-array-expression.ts index bfe93f73..8204ea6f 100644 --- a/src/ast-transform/transform-array-expression.ts +++ b/src/ast-transform/transform-array-expression.ts @@ -1,7 +1,7 @@ import { type LiteralArray } from '@angular/compiler'; import type * as babel from '@babel/types'; -import { type Transformer } from '../transform-ast.ts'; +import { type Transformer } from '../transform.ts'; export const visitLiteralArray = ( node: LiteralArray, diff --git a/src/ast-transform/transform-binary-expression.ts b/src/ast-transform/transform-binary-expression.ts index f2ccffcc..545da936 100644 --- a/src/ast-transform/transform-binary-expression.ts +++ b/src/ast-transform/transform-binary-expression.ts @@ -1,7 +1,7 @@ import { Binary } from '@angular/compiler'; import type * as babel from '@babel/types'; -import { type Transformer } from '../transform-ast.ts'; +import { type Transformer } from '../transform.ts'; const isAssignmentOperator = ( operator: Binary['operation'], diff --git a/src/ast-transform/transform-call-expression.ts b/src/ast-transform/transform-call-expression.ts index 313d5205..1ab50452 100644 --- a/src/ast-transform/transform-call-expression.ts +++ b/src/ast-transform/transform-call-expression.ts @@ -1,7 +1,7 @@ import { type Call, type SafeCall } from '@angular/compiler'; import type * as babel from '@babel/types'; -import { type Transformer } from '../transform-ast.ts'; +import { type Transformer } from '../transform.ts'; import { isOptionalObjectOrCallee } from './utilities.ts'; const callOptions = { optional: false } as const; diff --git a/src/ast-transform/transform-chained-expression.ts b/src/ast-transform/transform-chained-expression.ts index 6c92d1d5..80990d10 100644 --- a/src/ast-transform/transform-chained-expression.ts +++ b/src/ast-transform/transform-chained-expression.ts @@ -1,7 +1,7 @@ import { type Chain } from '@angular/compiler'; import type * as babel from '@babel/types'; -import { type Transformer } from '../transform-ast.ts'; +import { type Transformer } from '../transform.ts'; import type { NGChainedExpression } from '../types.ts'; export const visitChain = ( diff --git a/src/ast-transform/transform-conditional-expression.ts b/src/ast-transform/transform-conditional-expression.ts index c4afca14..42d53c23 100644 --- a/src/ast-transform/transform-conditional-expression.ts +++ b/src/ast-transform/transform-conditional-expression.ts @@ -1,7 +1,7 @@ import { type Conditional } from '@angular/compiler'; import type * as babel from '@babel/types'; -import { type Transformer } from '../transform-ast.ts'; +import { type Transformer } from '../transform.ts'; export const visitConditional = ( node: Conditional, diff --git a/src/ast-transform/transform-interpolation.ts b/src/ast-transform/transform-interpolation.ts index 1119c748..93f51f8c 100644 --- a/src/ast-transform/transform-interpolation.ts +++ b/src/ast-transform/transform-interpolation.ts @@ -1,6 +1,6 @@ import { type Interpolation } from '@angular/compiler'; -import { type Transformer } from '../transform-ast.ts'; +import { type Transformer } from '../transform.ts'; export const visitInterpolation = ( node: Interpolation, diff --git a/src/ast-transform/transform-member-expression.ts b/src/ast-transform/transform-member-expression.ts index 04a90c0b..97e734e8 100644 --- a/src/ast-transform/transform-member-expression.ts +++ b/src/ast-transform/transform-member-expression.ts @@ -7,7 +7,7 @@ import { } from '@angular/compiler'; import type * as babel from '@babel/types'; -import { type Transformer } from '../transform-ast.ts'; +import { type Transformer } from '../transform.ts'; import { isOptionalObjectOrCallee } from './utilities.ts'; const keyedReadOptions = { computed: true, optional: false } as const; diff --git a/src/ast-transform/transform-non-null-expression.ts b/src/ast-transform/transform-non-null-expression.ts index 8484a19e..aad14232 100644 --- a/src/ast-transform/transform-non-null-expression.ts +++ b/src/ast-transform/transform-non-null-expression.ts @@ -1,7 +1,7 @@ import { type NonNullAssert } from '@angular/compiler'; import type * as babel from '@babel/types'; -import { type Transformer } from '../transform-ast.ts'; +import { type Transformer } from '../transform.ts'; export const visitNonNullAssert = ( node: NonNullAssert, diff --git a/src/ast-transform/transform-object-expression.ts b/src/ast-transform/transform-object-expression.ts index e913daf8..8c84f152 100644 --- a/src/ast-transform/transform-object-expression.ts +++ b/src/ast-transform/transform-object-expression.ts @@ -1,7 +1,7 @@ import type * as angular from '@angular/compiler'; import type * as babel from '@babel/types'; -import { type Transformer } from '../transform-ast.ts'; +import { type Transformer } from '../transform.ts'; import type { NGNode, RawNGSpan } from '../types.ts'; export const visitLiteralMap = ( diff --git a/src/ast-transform/transform-parenthesized-expression.ts b/src/ast-transform/transform-parenthesized-expression.ts index 8b307ff0..0ebcf0c8 100644 --- a/src/ast-transform/transform-parenthesized-expression.ts +++ b/src/ast-transform/transform-parenthesized-expression.ts @@ -1,6 +1,6 @@ import { type ParenthesizedExpression } from '@angular/compiler'; -import { type Transformer } from '../transform-ast.ts'; +import { type Transformer } from '../transform.ts'; export const visitParenthesizedExpression = ( node: ParenthesizedExpression, diff --git a/src/ast-transform/transform-pipe-expression.ts b/src/ast-transform/transform-pipe-expression.ts index 25416603..552aea3e 100644 --- a/src/ast-transform/transform-pipe-expression.ts +++ b/src/ast-transform/transform-pipe-expression.ts @@ -1,7 +1,7 @@ import { type BindingPipe } from '@angular/compiler'; import type * as babel from '@babel/types'; -import { type Transformer } from '../transform-ast.ts'; +import { type Transformer } from '../transform.ts'; import type { NGPipeExpression } from '../types.ts'; export const visitPipe = ( diff --git a/src/ast-transform/transform-template-literal.ts b/src/ast-transform/transform-template-literal.ts index c16e4be0..f2eae19f 100644 --- a/src/ast-transform/transform-template-literal.ts +++ b/src/ast-transform/transform-template-literal.ts @@ -5,7 +5,7 @@ import { } from '@angular/compiler'; import type * as babel from '@babel/types'; -import { type Transformer } from '../transform-ast.ts'; +import { type Transformer } from '../transform.ts'; export const visitTaggedTemplateLiteral = ( node: TaggedTemplateLiteral, diff --git a/src/ast-transform/transform-unary-expression.ts b/src/ast-transform/transform-unary-expression.ts index 0c6c3774..a72a83a5 100644 --- a/src/ast-transform/transform-unary-expression.ts +++ b/src/ast-transform/transform-unary-expression.ts @@ -6,7 +6,7 @@ import { } from '@angular/compiler'; import type * as babel from '@babel/types'; -import { type Transformer } from '../transform-ast.ts'; +import { type Transformer } from '../transform.ts'; type VisitorPrefixNot = { node: PrefixNot; diff --git a/src/transform-ast.ts b/src/ast-transform/transform.ts similarity index 92% rename from src/transform-ast.ts rename to src/ast-transform/transform.ts index 7f35187a..797601d6 100644 --- a/src/transform-ast.ts +++ b/src/ast-transform/transform.ts @@ -1,8 +1,8 @@ import * as angular from '@angular/compiler'; -import { transformVisitor } from './ast-transform/visitor.ts'; -import { Source } from './source.ts'; -import type { NGEmptyExpression, NGNode, RawNGSpan } from './types.ts'; +import { Source } from '../source.ts'; +import type { NGEmptyExpression, NGNode, RawNGSpan } from '../types.ts'; +import { transformVisitor } from './visitor.ts'; class Transformer extends Source { node: angular.AST; diff --git a/src/transform-template-binding.ts b/src/transform-template-binding.ts index 6737a7f6..5973f0c4 100644 --- a/src/transform-template-binding.ts +++ b/src/transform-template-binding.ts @@ -4,8 +4,8 @@ import { VariableBinding as NGVariableBinding, } from '@angular/compiler'; +import { transform as transformAst } from './ast-transform/index.ts'; import { Source } from './source.ts'; -import { transform as transformNode } from './transform-ast.ts'; import type { NGMicrosyntax, NGMicrosyntaxAs, @@ -62,7 +62,7 @@ class TemplateBindingTransformer extends Source { } #transform(node: angular.AST) { - return transformNode(node, this.text) as T; + return transformAst(node, this.text) as T; } #removePrefix(string: string) { diff --git a/src/transform.ts b/src/transform.ts index 98ac70cd..3b3ff604 100644 --- a/src/transform.ts +++ b/src/transform.ts @@ -2,7 +2,7 @@ import type { AstParseResult, MicroSyntaxParseResult, } from './angular-parser.ts'; -import { transform as transformNode } from './transform-ast.ts'; +import { transform as transformAst } from './ast-transform/index.ts'; import { transform as transformTemplateBindings } from './transform-template-binding.ts'; function transformAstResult({ @@ -10,7 +10,7 @@ function transformAstResult({ text, comments, }: AstParseResult) { - return Object.assign(transformNode(ast, text), { comments }); + return Object.assign(transformAst(ast, text), { comments }); } function transformMicrosyntaxResult({ From e5105815788f7130c123745bd71a262bddf47fd8 Mon Sep 17 00:00:00 2001 From: fisker Date: Thu, 8 Jan 2026 23:29:18 +0800 Subject: [PATCH 11/16] transform AST with source --- src/angular-parser.ts | 2 ++ src/ast-transform/index.ts | 2 +- src/ast-transform/transform-ast-with-source.ts | 8 ++++++++ src/ast-transform/transform-parenthesized-expression.ts | 2 +- src/ast-transform/transform-unexpected-node.ts | 8 ++------ src/ast-transform/transform.ts | 8 ++++++-- src/ast-transform/transforms.ts | 1 + src/transform-template-binding.ts | 2 +- src/transform.ts | 8 ++------ 9 files changed, 24 insertions(+), 17 deletions(-) create mode 100644 src/ast-transform/transform-ast-with-source.ts diff --git a/src/angular-parser.ts b/src/angular-parser.ts index b9a307c8..c630de15 100644 --- a/src/angular-parser.ts +++ b/src/angular-parser.ts @@ -61,6 +61,8 @@ function createAngularParseFunction< ); } + console.log(result); + return { result, comments, text }; }; } diff --git a/src/ast-transform/index.ts b/src/ast-transform/index.ts index ddffe4fd..ecfd4b93 100644 --- a/src/ast-transform/index.ts +++ b/src/ast-transform/index.ts @@ -1 +1 @@ -export { transform } from './transform.ts'; +export { transform, transformAst } from './transform.ts'; diff --git a/src/ast-transform/transform-ast-with-source.ts b/src/ast-transform/transform-ast-with-source.ts new file mode 100644 index 00000000..d91e7909 --- /dev/null +++ b/src/ast-transform/transform-ast-with-source.ts @@ -0,0 +1,8 @@ +import { type ASTWithSource } from '@angular/compiler'; + +import { type Transformer } from './transform.ts'; + +export const visitASTWithSource = ( + node: ASTWithSource, + transformer: Transformer, +) => transformer.transformChild(node.ast); diff --git a/src/ast-transform/transform-parenthesized-expression.ts b/src/ast-transform/transform-parenthesized-expression.ts index 0ebcf0c8..adea04b2 100644 --- a/src/ast-transform/transform-parenthesized-expression.ts +++ b/src/ast-transform/transform-parenthesized-expression.ts @@ -1,6 +1,6 @@ import { type ParenthesizedExpression } from '@angular/compiler'; -import { type Transformer } from '../transform.ts'; +import { type Transformer } from './transform.ts'; export const visitParenthesizedExpression = ( node: ParenthesizedExpression, diff --git a/src/ast-transform/transform-unexpected-node.ts b/src/ast-transform/transform-unexpected-node.ts index d99dacf9..03cecb18 100644 --- a/src/ast-transform/transform-unexpected-node.ts +++ b/src/ast-transform/transform-unexpected-node.ts @@ -1,12 +1,8 @@ -import { type ASTWithSource, type ImplicitReceiver } from '@angular/compiler'; +import { type ImplicitReceiver } from '@angular/compiler'; -function transformUnexpectedNode( - node: T, -) { +function transformUnexpectedNode(node: T) { throw new Error(`Unexpected node type '${node.constructor.name}'`); } // Handled in `./transform-member-expression.ts` export const visitImplicitReceiver = transformUnexpectedNode; -// Unreachable -export const visitASTWithSource = transformUnexpectedNode; diff --git a/src/ast-transform/transform.ts b/src/ast-transform/transform.ts index 797601d6..4214b6e6 100644 --- a/src/ast-transform/transform.ts +++ b/src/ast-transform/transform.ts @@ -87,8 +87,12 @@ class Transformer extends Source { } } -const transform = (node: angular.AST, text: string) => { +const transformAst = (node: angular.AST, text: string) => { return Transformer.transform(node, text); }; -export { transform, Transformer }; +const transform = (ast: angular.ASTWithSource) => { + return Transformer.transform(ast, ast.source!); +}; + +export { transform, transformAst, Transformer }; diff --git a/src/ast-transform/transforms.ts b/src/ast-transform/transforms.ts index d3e376eb..ebe32c46 100644 --- a/src/ast-transform/transforms.ts +++ b/src/ast-transform/transforms.ts @@ -1,4 +1,5 @@ export * from './transform-array-expression.ts'; +export * from './transform-ast-with-source.ts'; export * from './transform-binary-expression.ts'; export * from './transform-call-expression.ts'; export * from './transform-chained-expression.ts'; diff --git a/src/transform-template-binding.ts b/src/transform-template-binding.ts index 5973f0c4..d7c27a74 100644 --- a/src/transform-template-binding.ts +++ b/src/transform-template-binding.ts @@ -4,7 +4,7 @@ import { VariableBinding as NGVariableBinding, } from '@angular/compiler'; -import { transform as transformAst } from './ast-transform/index.ts'; +import { transformAst } from './ast-transform/index.ts'; import { Source } from './source.ts'; import type { NGMicrosyntax, diff --git a/src/transform.ts b/src/transform.ts index 3b3ff604..f3b51f35 100644 --- a/src/transform.ts +++ b/src/transform.ts @@ -5,12 +5,8 @@ import type { import { transform as transformAst } from './ast-transform/index.ts'; import { transform as transformTemplateBindings } from './transform-template-binding.ts'; -function transformAstResult({ - result: { ast }, - text, - comments, -}: AstParseResult) { - return Object.assign(transformAst(ast, text), { comments }); +function transformAstResult({ result, comments }: AstParseResult) { + return Object.assign(transformAst(result), { comments }); } function transformMicrosyntaxResult({ From 20ed95f451d8163f41d502d0c3775a1226c39613 Mon Sep 17 00:00:00 2001 From: fisker Date: Fri, 9 Jan 2026 00:19:25 +0800 Subject: [PATCH 12/16] Simplify logic --- src/angular-parser.ts | 107 +++++++++--------- src/ast-transform/index.ts | 2 +- .../transform-array-expression.ts | 2 +- .../transform-binary-expression.ts | 2 +- .../transform-call-expression.ts | 2 +- .../transform-chained-expression.ts | 2 +- .../transform-conditional-expression.ts | 2 +- src/ast-transform/transform-interpolation.ts | 2 +- .../transform-member-expression.ts | 2 +- .../transform-non-null-expression.ts | 2 +- .../transform-object-expression.ts | 2 +- .../transform-pipe-expression.ts | 2 +- .../transform-template-literal.ts | 2 +- .../transform-unary-expression.ts | 2 +- src/ast-transform/transform.ts | 6 +- src/estree-parser.ts | 39 ++++--- src/transform-template-binding.ts | 8 +- src/transform.ts | 19 ---- 18 files changed, 101 insertions(+), 104 deletions(-) delete mode 100644 src/transform.ts diff --git a/src/angular-parser.ts b/src/angular-parser.ts index c630de15..c9b22554 100644 --- a/src/angular-parser.ts +++ b/src/angular-parser.ts @@ -11,22 +11,32 @@ import { import { type CommentLine } from './types.ts'; import { sourceSpanToLocationInformation } from './utils.ts'; +let parseSourceSpan: ParseSourceSpan; // https://github.com/angular/angular/blob/5e9707dc84e6590ec8c9d41e7d3be7deb2fa7c53/packages/compiler/test/expression_parser/utils/span.ts -function getFakeSpan(fileName = 'test.html') { - const file = new ParseSourceFile('', fileName); - const location = new ParseLocation(file, 0, 0, 0); - return new ParseSourceSpan(location, location); +function getParseSourceSpan() { + if (!parseSourceSpan) { + const file = new ParseSourceFile('', 'test.html'); + const location = new ParseLocation(file, -1, -1, -1); + parseSourceSpan = new ParseSourceSpan(location, location); + } + + return parseSourceSpan; +} + +let parser: Parser; +function getParser() { + return (parser ??= new Parser(new Lexer())); } const getCommentStart = (text: string): number | null => // @ts-expect-error -- need to call private _commentStart Parser.prototype._commentStart(text); -function extractComments(text: string, shouldExtractComment: boolean) { - const commentStart = shouldExtractComment ? getCommentStart(text) : null; +function extractComments(text: string) { + const commentStart = getCommentStart(text); if (commentStart === null) { - return { text, comments: [] }; + return []; } const comment: CommentLine = { @@ -38,55 +48,48 @@ function extractComments(text: string, shouldExtractComment: boolean) { }), }; - return { text, comments: [comment] }; + return [comment]; } -function createAngularParseFunction< - T extends ASTWithSource | TemplateBindingParseResult, ->(parse: (text: string, parser: Parser) => T, shouldExtractComment = true) { - return (originalText: string) => { - const lexer = new Lexer(); - const parser = new Parser(lexer); - - const { text, comments } = extractComments( - originalText, - shouldExtractComment, +function throwErrors< + ResultType extends ASTWithSource | TemplateBindingParseResult, +>(result: ResultType) { + if (result.errors.length !== 0) { + const [{ message }] = result.errors; + throw new SyntaxError( + message.replace(/^Parser Error: | at column \d+ in [^]*$/g, ''), ); - const result = parse(text, parser); - - if (result.errors.length !== 0) { - const [{ message }] = result.errors; - throw new SyntaxError( - message.replace(/^Parser Error: | at column \d+ in [^]*$/g, ''), - ); - } - - console.log(result); + } - return { result, comments, text }; - }; + return result; } -export const parseBinding = createAngularParseFunction((text, parser) => - parser.parseBinding(text, getFakeSpan(), 0), +const createAstParser = + ( + name: + | 'parseBinding' + | 'parseSimpleBinding' + | 'parseAction' + | 'parseInterpolationExpression', + ) => + (text: string) => ({ + result: throwErrors( + getParser()[name](text, getParseSourceSpan(), 0), + ), + text, + comments: extractComments(text), + }); + +export const parseAction = createAstParser('parseAction'); +export const parseBinding = createAstParser('parseBinding'); +export const parseSimpleBinding = createAstParser('parseSimpleBinding'); +export const parseInterpolationExpression = createAstParser( + 'parseInterpolationExpression', ); - -export const parseSimpleBinding = createAngularParseFunction((text, parser) => - parser.parseSimpleBinding(text, getFakeSpan(), 0), -); - -export const parseAction = createAngularParseFunction((text, parser) => - parser.parseAction(text, getFakeSpan(), 0), -); - -export const parseInterpolationExpression = createAngularParseFunction( - (text, parser) => parser.parseInterpolationExpression(text, getFakeSpan(), 0), -); - -export const parseTemplateBindings = createAngularParseFunction( - (text, parser) => parser.parseTemplateBindings('', text, getFakeSpan(), 0, 0), - /* shouldExtractComment */ false, -); - -export type AstParseResult = ReturnType; -export type MicroSyntaxParseResult = ReturnType; +export const parseTemplateBindings = (text: string) => ({ + result: throwErrors( + getParser().parseTemplateBindings('', text, getParseSourceSpan(), 0, 0), + ), + text, + comments: [], +}); diff --git a/src/ast-transform/index.ts b/src/ast-transform/index.ts index ecfd4b93..c87784e0 100644 --- a/src/ast-transform/index.ts +++ b/src/ast-transform/index.ts @@ -1 +1 @@ -export { transform, transformAst } from './transform.ts'; +export { transformAst, transformAstNode } from './transform.ts'; diff --git a/src/ast-transform/transform-array-expression.ts b/src/ast-transform/transform-array-expression.ts index 8204ea6f..ead723ad 100644 --- a/src/ast-transform/transform-array-expression.ts +++ b/src/ast-transform/transform-array-expression.ts @@ -1,7 +1,7 @@ import { type LiteralArray } from '@angular/compiler'; import type * as babel from '@babel/types'; -import { type Transformer } from '../transform.ts'; +import { type Transformer } from './transform.ts'; export const visitLiteralArray = ( node: LiteralArray, diff --git a/src/ast-transform/transform-binary-expression.ts b/src/ast-transform/transform-binary-expression.ts index 545da936..d67200e4 100644 --- a/src/ast-transform/transform-binary-expression.ts +++ b/src/ast-transform/transform-binary-expression.ts @@ -1,7 +1,7 @@ import { Binary } from '@angular/compiler'; import type * as babel from '@babel/types'; -import { type Transformer } from '../transform.ts'; +import { type Transformer } from './transform.ts'; const isAssignmentOperator = ( operator: Binary['operation'], diff --git a/src/ast-transform/transform-call-expression.ts b/src/ast-transform/transform-call-expression.ts index 1ab50452..b7e18924 100644 --- a/src/ast-transform/transform-call-expression.ts +++ b/src/ast-transform/transform-call-expression.ts @@ -1,7 +1,7 @@ import { type Call, type SafeCall } from '@angular/compiler'; import type * as babel from '@babel/types'; -import { type Transformer } from '../transform.ts'; +import { type Transformer } from './transform.ts'; import { isOptionalObjectOrCallee } from './utilities.ts'; const callOptions = { optional: false } as const; diff --git a/src/ast-transform/transform-chained-expression.ts b/src/ast-transform/transform-chained-expression.ts index 80990d10..edada685 100644 --- a/src/ast-transform/transform-chained-expression.ts +++ b/src/ast-transform/transform-chained-expression.ts @@ -1,7 +1,7 @@ import { type Chain } from '@angular/compiler'; import type * as babel from '@babel/types'; -import { type Transformer } from '../transform.ts'; +import { type Transformer } from './transform.ts'; import type { NGChainedExpression } from '../types.ts'; export const visitChain = ( diff --git a/src/ast-transform/transform-conditional-expression.ts b/src/ast-transform/transform-conditional-expression.ts index 42d53c23..215d5454 100644 --- a/src/ast-transform/transform-conditional-expression.ts +++ b/src/ast-transform/transform-conditional-expression.ts @@ -1,7 +1,7 @@ import { type Conditional } from '@angular/compiler'; import type * as babel from '@babel/types'; -import { type Transformer } from '../transform.ts'; +import { type Transformer } from './transform.ts'; export const visitConditional = ( node: Conditional, diff --git a/src/ast-transform/transform-interpolation.ts b/src/ast-transform/transform-interpolation.ts index 93f51f8c..526d4ac4 100644 --- a/src/ast-transform/transform-interpolation.ts +++ b/src/ast-transform/transform-interpolation.ts @@ -1,6 +1,6 @@ import { type Interpolation } from '@angular/compiler'; -import { type Transformer } from '../transform.ts'; +import { type Transformer } from './transform.ts'; export const visitInterpolation = ( node: Interpolation, diff --git a/src/ast-transform/transform-member-expression.ts b/src/ast-transform/transform-member-expression.ts index 97e734e8..a2ecf01e 100644 --- a/src/ast-transform/transform-member-expression.ts +++ b/src/ast-transform/transform-member-expression.ts @@ -7,7 +7,7 @@ import { } from '@angular/compiler'; import type * as babel from '@babel/types'; -import { type Transformer } from '../transform.ts'; +import { type Transformer } from './transform.ts'; import { isOptionalObjectOrCallee } from './utilities.ts'; const keyedReadOptions = { computed: true, optional: false } as const; diff --git a/src/ast-transform/transform-non-null-expression.ts b/src/ast-transform/transform-non-null-expression.ts index aad14232..6bb53f0d 100644 --- a/src/ast-transform/transform-non-null-expression.ts +++ b/src/ast-transform/transform-non-null-expression.ts @@ -1,7 +1,7 @@ import { type NonNullAssert } from '@angular/compiler'; import type * as babel from '@babel/types'; -import { type Transformer } from '../transform.ts'; +import { type Transformer } from './transform.ts'; export const visitNonNullAssert = ( node: NonNullAssert, diff --git a/src/ast-transform/transform-object-expression.ts b/src/ast-transform/transform-object-expression.ts index 8c84f152..39b73390 100644 --- a/src/ast-transform/transform-object-expression.ts +++ b/src/ast-transform/transform-object-expression.ts @@ -1,7 +1,7 @@ import type * as angular from '@angular/compiler'; import type * as babel from '@babel/types'; -import { type Transformer } from '../transform.ts'; +import { type Transformer } from './transform.ts'; import type { NGNode, RawNGSpan } from '../types.ts'; export const visitLiteralMap = ( diff --git a/src/ast-transform/transform-pipe-expression.ts b/src/ast-transform/transform-pipe-expression.ts index 552aea3e..b4cd6a46 100644 --- a/src/ast-transform/transform-pipe-expression.ts +++ b/src/ast-transform/transform-pipe-expression.ts @@ -1,7 +1,7 @@ import { type BindingPipe } from '@angular/compiler'; import type * as babel from '@babel/types'; -import { type Transformer } from '../transform.ts'; +import { type Transformer } from './transform.ts'; import type { NGPipeExpression } from '../types.ts'; export const visitPipe = ( diff --git a/src/ast-transform/transform-template-literal.ts b/src/ast-transform/transform-template-literal.ts index f2eae19f..dcdbeca4 100644 --- a/src/ast-transform/transform-template-literal.ts +++ b/src/ast-transform/transform-template-literal.ts @@ -5,7 +5,7 @@ import { } from '@angular/compiler'; import type * as babel from '@babel/types'; -import { type Transformer } from '../transform.ts'; +import { type Transformer } from './transform.ts'; export const visitTaggedTemplateLiteral = ( node: TaggedTemplateLiteral, diff --git a/src/ast-transform/transform-unary-expression.ts b/src/ast-transform/transform-unary-expression.ts index a72a83a5..8bf0fde6 100644 --- a/src/ast-transform/transform-unary-expression.ts +++ b/src/ast-transform/transform-unary-expression.ts @@ -6,7 +6,7 @@ import { } from '@angular/compiler'; import type * as babel from '@babel/types'; -import { type Transformer } from '../transform.ts'; +import { type Transformer } from './transform.ts'; type VisitorPrefixNot = { node: PrefixNot; diff --git a/src/ast-transform/transform.ts b/src/ast-transform/transform.ts index 4214b6e6..9e988b5c 100644 --- a/src/ast-transform/transform.ts +++ b/src/ast-transform/transform.ts @@ -87,12 +87,12 @@ class Transformer extends Source { } } -const transformAst = (node: angular.AST, text: string) => { +const transformAstNode = (node: angular.AST, text: string) => { return Transformer.transform(node, text); }; -const transform = (ast: angular.ASTWithSource) => { +const transformAst = (ast: angular.ASTWithSource) => { return Transformer.transform(ast, ast.source!); }; -export { transform, transformAst, Transformer }; +export { transformAst, transformAstNode, Transformer }; diff --git a/src/estree-parser.ts b/src/estree-parser.ts index a84bd67e..25776cbe 100644 --- a/src/estree-parser.ts +++ b/src/estree-parser.ts @@ -1,17 +1,30 @@ import * as angularParser from './angular-parser.ts'; -import { transformAstResult, transformMicrosyntaxResult } from './transform.ts'; +import { transformAst } from './ast-transform/index.ts'; +import { transformTemplateBindings } from './transform-template-binding.ts'; -export const parseBinding = (text: string) => - transformAstResult(angularParser.parseBinding(text)); - -export const parseSimpleBinding = (text: string) => - transformAstResult(angularParser.parseSimpleBinding(text)); - -export const parseInterpolationExpression = (text: string) => - transformAstResult(angularParser.parseInterpolationExpression(text)); - -export const parseAction = (text: string) => - transformAstResult(angularParser.parseAction(text)); +const createAstParser = + ( + parse: + | typeof angularParser.parseBinding + | typeof angularParser.parseSimpleBinding + | typeof angularParser.parseInterpolationExpression + | typeof angularParser.parseAction, + ) => + (text: string) => { + const { result, comments } = parse(text); + return Object.assign(transformAst(result), { comments }); + }; +export const parseAction = createAstParser(angularParser.parseAction); +export const parseBinding = createAstParser(angularParser.parseBinding); +export const parseSimpleBinding = createAstParser( + angularParser.parseSimpleBinding, +); +export const parseInterpolationExpression = createAstParser( + angularParser.parseInterpolationExpression, +); export const parseTemplateBindings = (text: string) => - transformMicrosyntaxResult(angularParser.parseTemplateBindings(text)); + transformTemplateBindings( + angularParser.parseTemplateBindings(text).result.templateBindings, + text, + ); diff --git a/src/transform-template-binding.ts b/src/transform-template-binding.ts index d7c27a74..7a009a03 100644 --- a/src/transform-template-binding.ts +++ b/src/transform-template-binding.ts @@ -4,7 +4,7 @@ import { VariableBinding as NGVariableBinding, } from '@angular/compiler'; -import { transformAst } from './ast-transform/index.ts'; +import { transformAstNode } from './ast-transform/index.ts'; import { Source } from './source.ts'; import type { NGMicrosyntax, @@ -62,7 +62,7 @@ class TemplateBindingTransformer extends Source { } #transform(node: angular.AST) { - return transformAst(node, this.text) as T; + return transformAstNode(node, this.text) as T; } #removePrefix(string: string) { @@ -277,11 +277,11 @@ class TemplateBindingTransformer extends Source { } } -function transform( +function transformTemplateBindings( rawTemplateBindings: angular.TemplateBinding[], text: string, ) { return new TemplateBindingTransformer(rawTemplateBindings, text).expressions; } -export { transform }; +export { transformTemplateBindings }; diff --git a/src/transform.ts b/src/transform.ts deleted file mode 100644 index f3b51f35..00000000 --- a/src/transform.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { - AstParseResult, - MicroSyntaxParseResult, -} from './angular-parser.ts'; -import { transform as transformAst } from './ast-transform/index.ts'; -import { transform as transformTemplateBindings } from './transform-template-binding.ts'; - -function transformAstResult({ result, comments }: AstParseResult) { - return Object.assign(transformAst(result), { comments }); -} - -function transformMicrosyntaxResult({ - result: { templateBindings }, - text, -}: MicroSyntaxParseResult) { - return transformTemplateBindings(templateBindings, text); -} - -export { transformAstResult, transformMicrosyntaxResult }; From 825fd9ddc69926088a97768aa9bfab39e8885a4d Mon Sep 17 00:00:00 2001 From: fisker Date: Fri, 9 Jan 2026 00:28:11 +0800 Subject: [PATCH 13/16] Minor refactor --- src/ast-transform/transform.ts | 66 ++++++++++++++++++++++------------ 1 file changed, 44 insertions(+), 22 deletions(-) diff --git a/src/ast-transform/transform.ts b/src/ast-transform/transform.ts index 9e988b5c..a052cddf 100644 --- a/src/ast-transform/transform.ts +++ b/src/ast-transform/transform.ts @@ -1,21 +1,26 @@ -import * as angular from '@angular/compiler'; +import { + type AST, + type ASTWithSource, + EmptyExpr, + ParenthesizedExpression, +} from '@angular/compiler'; import { Source } from '../source.ts'; import type { NGEmptyExpression, NGNode, RawNGSpan } from '../types.ts'; import { transformVisitor } from './visitor.ts'; -class Transformer extends Source { - node: angular.AST; - ancestors: angular.AST[]; +class NodeTransformer extends Source { + node: AST; + ancestors: AST[]; constructor({ node, text, ancestors, }: { - node: angular.AST; + node: AST; text: string; - ancestors: angular.AST[]; + ancestors: AST[]; }) { super(text); this.node = node; @@ -24,12 +29,12 @@ class Transformer extends Source { create( properties: Partial & { type: T['type'] }, - location: angular.AST | RawNGSpan | [number, number], - ancestors: angular.AST[], + location: AST | RawNGSpan | [number, number], + ancestors: AST[], ) { const node = super.createNode(properties, location); - if (ancestors[0] instanceof angular.ParenthesizedExpression) { + if (ancestors[0] instanceof ParenthesizedExpression) { node.extra = { ...node.extra, parenthesized: true, @@ -41,27 +46,27 @@ class Transformer extends Source { createNode( properties: Partial & { type: T['type'] }, - location: angular.AST | RawNGSpan | [number, number] = this.node, - ancestorsToCreate: angular.AST[] = this.ancestors, + location: AST | RawNGSpan | [number, number] = this.node, + ancestorsToCreate: AST[] = this.ancestors, ) { return this.create(properties, location, ancestorsToCreate); } - transformChild(child: angular.AST) { - return new Transformer({ + transformChild(child: AST) { + return new NodeTransformer({ node: child, ancestors: [this.node, ...this.ancestors], text: this.text, }).transform(); } - transformChildren(children: angular.AST[]) { + transformChildren(children: AST[]) { return children.map((child) => this.transformChild(child)); } transform() { const { node } = this; - if (node instanceof angular.EmptyExpr) { + if (node instanceof EmptyExpr) { return this.createNode( { type: 'NGEmptyExpression' }, node.sourceSpan, @@ -82,17 +87,34 @@ class Transformer extends Source { return estreeNode as T; } - static transform(node: angular.AST, text: string) { - return new Transformer({ node, text, ancestors: [] }).transform(); + static transform(node: AST, text: string) { + return new NodeTransformer({ node, text, ancestors: [] }).transform(); } } -const transformAstNode = (node: angular.AST, text: string) => { - return Transformer.transform(node, text); +class AstTransformer { + #ast; + + constructor(ast: ASTWithSource) { + this.#ast = ast; + } + + transform() { + return NodeTransformer.transform(this.#ast, this.#ast.source!); + } +} + +const transformAstNode = (node: AST, text: string) => { + return NodeTransformer.transform(node, text); }; -const transformAst = (ast: angular.ASTWithSource) => { - return Transformer.transform(ast, ast.source!); +const transformAst = (ast: ASTWithSource) => { + return new AstTransformer(ast).transform(); }; -export { transformAst, transformAstNode, Transformer }; +export { + NodeTransformer, + transformAst, + transformAstNode, + NodeTransformer as Transformer, +}; From 8fec49ae23a3ead851ebd95a0adcf8e12b382a1e Mon Sep 17 00:00:00 2001 From: fisker Date: Fri, 9 Jan 2026 00:45:15 +0800 Subject: [PATCH 14/16] Linting --- src/ast-transform/transform-object-expression.ts | 2 +- src/transform-template-binding.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ast-transform/transform-object-expression.ts b/src/ast-transform/transform-object-expression.ts index 39b73390..db084a35 100644 --- a/src/ast-transform/transform-object-expression.ts +++ b/src/ast-transform/transform-object-expression.ts @@ -1,8 +1,8 @@ import type * as angular from '@angular/compiler'; import type * as babel from '@babel/types'; -import { type Transformer } from './transform.ts'; import type { NGNode, RawNGSpan } from '../types.ts'; +import { type Transformer } from './transform.ts'; export const visitLiteralMap = ( node: angular.LiteralMap, diff --git a/src/transform-template-binding.ts b/src/transform-template-binding.ts index 7a009a03..3c0be941 100644 --- a/src/transform-template-binding.ts +++ b/src/transform-template-binding.ts @@ -58,7 +58,7 @@ class TemplateBindingTransformer extends Source { properties: Partial & { type: T['type'] }, location: angular.AST | RawNGSpan | [number, number], ) { - return this.createNode(properties, location); + return super.createNode(properties, location); } #transform(node: angular.AST) { From 8b0ee9bf2f4d546d16fb9724edcf725a08922acd Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Thu, 8 Jan 2026 16:51:52 +0000 Subject: [PATCH 15/16] Apply automated changes --- src/ast-transform/transform-chained-expression.ts | 2 +- src/ast-transform/transform-pipe-expression.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ast-transform/transform-chained-expression.ts b/src/ast-transform/transform-chained-expression.ts index edada685..152404b3 100644 --- a/src/ast-transform/transform-chained-expression.ts +++ b/src/ast-transform/transform-chained-expression.ts @@ -1,8 +1,8 @@ import { type Chain } from '@angular/compiler'; import type * as babel from '@babel/types'; -import { type Transformer } from './transform.ts'; import type { NGChainedExpression } from '../types.ts'; +import { type Transformer } from './transform.ts'; export const visitChain = ( node: Chain, diff --git a/src/ast-transform/transform-pipe-expression.ts b/src/ast-transform/transform-pipe-expression.ts index b4cd6a46..0ea63d0a 100644 --- a/src/ast-transform/transform-pipe-expression.ts +++ b/src/ast-transform/transform-pipe-expression.ts @@ -1,8 +1,8 @@ import { type BindingPipe } from '@angular/compiler'; import type * as babel from '@babel/types'; -import { type Transformer } from './transform.ts'; import type { NGPipeExpression } from '../types.ts'; +import { type Transformer } from './transform.ts'; export const visitPipe = ( node: BindingPipe, From 4750854e7ed4b199eba4f45772d890ee1ba4ff35 Mon Sep 17 00:00:00 2001 From: fisker Date: Fri, 9 Jan 2026 01:03:11 +0800 Subject: [PATCH 16/16] Coverage --- src/ast-transform/transform-unexpected-node.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/ast-transform/transform-unexpected-node.ts b/src/ast-transform/transform-unexpected-node.ts index 03cecb18..4031a398 100644 --- a/src/ast-transform/transform-unexpected-node.ts +++ b/src/ast-transform/transform-unexpected-node.ts @@ -1,8 +1,9 @@ import { type ImplicitReceiver } from '@angular/compiler'; -function transformUnexpectedNode(node: T) { +/* c8 ignore next @preserve */ +const transformUnexpectedNode = (node: T) => { throw new Error(`Unexpected node type '${node.constructor.name}'`); -} +}; // Handled in `./transform-member-expression.ts` export const visitImplicitReceiver = transformUnexpectedNode;