diff --git a/eslint.config.js b/eslint.config.js index ab394969..736cca87 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -6,7 +6,7 @@ import globals from 'globals'; import tseslint from 'typescript-eslint'; export default tseslint.config( - { ignores: ['coverage/', 'lib/', '**/.yarn/**', '**/.pnp.*'] }, + { ignores: ['coverage/', 'lib/', '**/.yarn/**', '**/.pnp.*', 'dist*/'] }, eslintPluginJs.configs.recommended, ...tseslint.configs.recommended, eslintConfigPrettier, diff --git a/prettier.config.js b/prettier.config.js index a1a872dc..8dfde402 100644 --- a/prettier.config.js +++ b/prettier.config.js @@ -1,3 +1,7 @@ +/** + * @see https://prettier.io/docs/configuration + * @type {import("prettier").Config} + */ export default { singleQuote: true, trailingComma: 'all', diff --git a/src/angular-parser.ts b/src/angular-parser.ts index 612ced36..2705f9d3 100644 --- a/src/angular-parser.ts +++ b/src/angular-parser.ts @@ -38,10 +38,7 @@ function extractComments(text: string, shouldExtractComment: boolean) { }), }; - return { - text: text.slice(0, commentStart), - comments: [comment], - }; + return { text: text.slice(0, commentStart), comments: [comment] }; } function createAngularParseFunction< diff --git a/src/source.ts b/src/source.ts index c79701ba..c314e541 100644 --- a/src/source.ts +++ b/src/source.ts @@ -1,3 +1,4 @@ +import * as angular from '@angular/compiler'; import type * as babel from '@babel/types'; import type { LocationInformation, NGNode, RawNGSpan } from './types.ts'; @@ -27,11 +28,26 @@ export class Source { } createNode( - properties: Partial & { type: T['type'] } & RawNGSpan, + properties: Partial & { type: T['type'] }, + location: angular.AST | RawNGSpan | [number, number], ) { + let start: number; + let end: number; + let range: [number, number]; + if (Array.isArray(location)) { + range = location; + [start, end] = location; + } else { + ({ start, end } = + location instanceof angular.AST ? location.sourceSpan : location); + range = [start, end]; + } + const node = { + start, + end, + range, ...properties, - range: [properties.start, properties.end], } as T & LocationInformation; switch (node.type) { diff --git a/src/transform-node.ts b/src/transform-node.ts index 26ad0493..a0169ba3 100644 --- a/src/transform-node.ts +++ b/src/transform-node.ts @@ -45,10 +45,11 @@ class Transformer extends Source { } #create( - properties: Partial & { type: T['type'] } & RawNGSpan, + properties: Partial & { type: T['type'] }, + location: angular.AST | RawNGSpan | [number, number], ancestors: angular.AST[], ) { - const node = super.createNode(properties); + const node = super.createNode(properties, location); if (ancestors[0] instanceof angular.ParenthesizedExpression) { node.extra = { @@ -62,10 +63,15 @@ class Transformer extends Source { #transform(node: angular.AST, options: NodeTransformOptions): NGNode { const ancestors = options.ancestors; - const childTransformOptions = { - ...options, - ancestors: [node, ...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; @@ -75,160 +81,105 @@ class Transformer extends Source { throw new Error("Unexpected 'Interpolation'"); } - return this.transform(expressions[0], childTransformOptions); + return transformChild(expressions[0]); } if (node instanceof angular.Unary) { - return this.#create( - { - type: 'UnaryExpression', - prefix: true, - argument: this.transform(node.expr), - operator: node.operator as '-' | '+', - ...node.sourceSpan, - }, - ancestors, - ); + 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] = [node.left, node.right].map((node) => - this.transform(node, childTransformOptions), - ); + const [left, right] = transformChildren([ + node.left, + node.right, + ]); if (operator === '&&' || operator === '||' || operator === '??') { - return this.#create( - { - type: 'LogicalExpression', - operator: operator as babel.LogicalExpression['operator'], - left, - right, - ...node.sourceSpan, - }, - ancestors, - ); + return createNode({ + type: 'LogicalExpression', + operator: operator as babel.LogicalExpression['operator'], + left, + right, + }); } if (angular.Binary.isAssignmentOperation(operator)) { - return this.#create( - { - type: 'AssignmentExpression', - left: left as babel.MemberExpression, - right, - operator: operator as babel.AssignmentExpression['operator'], - ...node.sourceSpan, - }, - ancestors, - ); + return createNode({ + type: 'AssignmentExpression', + left: left as babel.MemberExpression, + right, + operator: operator as babel.AssignmentExpression['operator'], + }); } - return this.#create( - { - left, - right, - type: 'BinaryExpression', - operator: operator as babel.BinaryExpression['operator'], - ...node.sourceSpan, - }, - ancestors, - ); + return createNode({ + left, + right, + type: 'BinaryExpression', + operator: operator as babel.BinaryExpression['operator'], + }); } if (node instanceof angular.BindingPipe) { const { name } = node; - const left = this.transform( - node.exp, - childTransformOptions, - ); + const left = transformChild(node.exp); const leftEnd = node.exp.sourceSpan.end; const rightStart = super.getCharacterIndex( /\S/, super.getCharacterIndex('|', leftEnd) + 1, ); - const right = this.#create( - { - type: 'Identifier', - name, - start: rightStart, - end: rightStart + name.length, - }, - ancestors, - ); - const arguments_ = node.args.map((node) => - this.transform(node, childTransformOptions), - ); - return this.#create( - { - type: 'NGPipeExpression', - left, - right, - arguments: arguments_, - ...node.sourceSpan, - }, - ancestors, - ); + const right = createNode({ type: 'Identifier', name }, [ + rightStart, + rightStart + name.length, + ]); + const arguments_ = transformChildren(node.args); + return createNode({ + type: 'NGPipeExpression', + left, + right, + arguments: arguments_, + }); } if (node instanceof angular.Chain) { - return this.#create( - { - type: 'NGChainedExpression', - expressions: node.expressions.map((node) => - this.transform(node, childTransformOptions), - ), - ...node.sourceSpan, - }, - ancestors, - ); + return createNode({ + type: 'NGChainedExpression', + expressions: transformChildren(node.expressions), + }); } if (node instanceof angular.Conditional) { - const [test, consequent, alternate] = [ - node.condition, - node.trueExp, - node.falseExp, - ].map((node) => - this.transform(node, childTransformOptions), + const [test, consequent, alternate] = transformChildren( + [node.condition, node.trueExp, node.falseExp], ); - return this.#create( - { - type: 'ConditionalExpression', - test, - consequent, - alternate, - ...node.sourceSpan, - }, - ancestors, - ); + return createNode({ + type: 'ConditionalExpression', + test, + consequent, + alternate, + }); } if (node instanceof angular.EmptyExpr) { - return this.#create( - { type: 'NGEmptyExpression', ...node.sourceSpan }, - ancestors, - ); + return createNode({ type: 'NGEmptyExpression' }); } if (node instanceof angular.ImplicitReceiver) { - return this.#create( - { type: 'ThisExpression', ...node.sourceSpan }, - ancestors, - ); + return createNode({ type: 'ThisExpression' }); } if (node instanceof angular.LiteralArray) { - return this.#create( - { - type: 'ArrayExpression', - elements: node.expressions.map((node) => - this.transform(node, childTransformOptions), - ), - ...node.sourceSpan, - }, - ancestors, - ); + return createNode({ + type: 'ArrayExpression', + elements: transformChildren(node.expressions), + }); } if (node instanceof angular.LiteralMap) { @@ -251,81 +202,63 @@ class Transformer extends Source { /\S/, super.getCharacterLastIndex(':', valueStart - 1) - 1, ) + 1; - const keySpan = { start: keyStart, end: keyEnd }; const tKey = quoted - ? this.#create( - { - type: 'StringLiteral', - value: key, - ...keySpan, - }, + ? createNode( + { type: 'StringLiteral', value: key }, + [keyStart, keyEnd], [], ) - : this.#create( - { - type: 'Identifier', - name: key, - ...keySpan, - }, + : createNode( + { type: 'Identifier', name: key }, + [keyStart, keyEnd], [], ); const shorthand = tKey.end < tKey.start || keyStart === valueStart; - const value = this.transform( - values[index], - childTransformOptions, - ); + const value = transformChild(values[index]); - return this.#create( + return createNode( { type: 'ObjectProperty', key: tKey, value, shorthand, computed: false, - start: tKey.start, - end: valueEnd, }, + [tKey.start, valueEnd], [], ); }); - return this.#create( - { - type: 'ObjectExpression', - properties: tProperties, - ...node.sourceSpan, - }, - ancestors, - ); + return createNode({ + type: 'ObjectExpression', + properties: tProperties, + }); } if (node instanceof angular.LiteralPrimitive) { const { value } = node; switch (typeof value) { case 'boolean': - return this.#create( - { type: 'BooleanLiteral', value, ...node.sourceSpan }, - ancestors, - ); + return createNode({ + type: 'BooleanLiteral', + value, + }); case 'number': - return this.#create( - { type: 'NumericLiteral', value, ...node.sourceSpan }, - ancestors, - ); + return createNode({ + type: 'NumericLiteral', + value, + }); case 'object': - return this.#create( - { type: 'NullLiteral', ...node.sourceSpan }, - ancestors, - ); + return createNode({ type: 'NullLiteral' }); case 'string': - return this.#create( - { type: 'StringLiteral', value, ...node.sourceSpan }, - ancestors, - ); + return createNode({ + type: 'StringLiteral', + value, + }); case 'undefined': - return this.#create( - { type: 'Identifier', name: 'undefined', ...node.sourceSpan }, - ancestors, - ); + return createNode({ + type: 'Identifier', + name: 'undefined', + }); /* c8 ignore next 4 */ default: throw new Error( @@ -335,52 +268,37 @@ class Transformer extends Source { } if (node instanceof angular.RegularExpressionLiteral) { - return this.#create( - { - type: 'RegExpLiteral', - pattern: node.body, - flags: node.flags ?? '', - ...node.sourceSpan, - }, - ancestors, - ); + return createNode({ + type: 'RegExpLiteral', + pattern: node.body, + flags: node.flags ?? '', + }); } if (node instanceof angular.Call || node instanceof angular.SafeCall) { - const arguments_ = node.args.map((node) => - this.transform(node, childTransformOptions), - ); - const callee = this.transform(node.receiver); + 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 this.#create( - { - type: nodeType, - callee, - arguments: arguments_, - ...(nodeType === 'OptionalCallExpression' - ? { optional: isOptional } - : undefined), - ...node.sourceSpan, - }, - ancestors, - ); + return createNode({ + type: nodeType, + callee, + arguments: arguments_, + ...(nodeType === 'OptionalCallExpression' + ? { optional: isOptional } + : undefined), + }); } if (node instanceof angular.NonNullAssert) { - const expression = this.transform(node.expression); - return this.#create( - { - type: 'TSNonNullExpression', - expression: expression, - ...node.sourceSpan, - }, - ancestors, - ); + return createNode({ + type: 'TSNonNullExpression', + expression: transformChild(node.expression), + }); } if ( @@ -420,18 +338,16 @@ class Transformer extends Source { start = index; } - const expression = this.transform(node.expression); + const expression = transformChild(node.expression); - return this.#create( + return createNode( { type: 'UnaryExpression', prefix: true, operator, argument: expression, - start, - end: node.sourceSpan.end, }, - ancestors, + [start, node.sourceSpan.end], ); } @@ -455,17 +371,14 @@ class Transformer extends Source { let property; if (isComputed) { isImplicitThis = node.sourceSpan.start === node.key.sourceSpan.start; - property = this.transform(node.key); + property = transformChild(node.key); } else { const { name, nameSpan } = node; isImplicitThis = node.sourceSpan.start === nameSpan.start; - property = this.#create( - { - type: 'Identifier', - name, - ...node.nameSpan, - }, + property = createNode( + { type: 'Identifier', name }, + node.nameSpan, isImplicitThis ? ancestors : [], ); } @@ -474,75 +387,50 @@ class Transformer extends Source { return property; } - const object = this.transform(receiver); + const object = transformChild(receiver); const isOptionalObject = isOptionalObjectOrCallee(object); - const commonProps = { - property, - object, - ...node.sourceSpan, - }; - if (isOptional || isOptionalObject) { - return this.#create( - { - type: 'OptionalMemberExpression', - optional: isOptional || !isOptionalObject, - computed: isComputed, - ...commonProps, - }, - ancestors, - ); + return createNode({ + type: 'OptionalMemberExpression', + optional: isOptional || !isOptionalObject, + computed: isComputed, + property, + object, + }); } if (isComputed) { - return this.#create( - { - type: 'MemberExpression', - ...commonProps, - computed: true, - }, - ancestors, - ); + return createNode({ + type: 'MemberExpression', + property, + object, + computed: true, + }); } - return this.#create( - { - type: 'MemberExpression', - ...commonProps, - computed: false, - property: property as babel.MemberExpressionNonComputed['property'], - }, - ancestors, - ); + return createNode({ + type: 'MemberExpression', + object, + property: property as babel.MemberExpressionNonComputed['property'], + computed: false, + }); } if (node instanceof angular.TaggedTemplateLiteral) { - return this.#create( - { - type: 'TaggedTemplateExpression', - tag: this.transform(node.tag), - quasi: this.transform(node.template), - ...node.sourceSpan, - }, - ancestors, - ); + return createNode({ + type: 'TaggedTemplateExpression', + tag: transformChild(node.tag), + quasi: transformChild(node.template), + }); } if (node instanceof angular.TemplateLiteral) { - return this.#create( - { - type: 'TemplateLiteral', - quasis: node.elements.map((element) => - this.transform(element, childTransformOptions), - ), - expressions: node.expressions.map((expression) => - this.transform(expression, childTransformOptions), - ), - ...node.sourceSpan, - }, - ancestors, - ); + return createNode({ + type: 'TemplateLiteral', + quasis: transformChildren(node.elements), + expressions: transformChildren(node.expressions), + }); } if (node instanceof angular.TemplateLiteralElement) { @@ -556,23 +444,18 @@ class Transformer extends Source { const start = node.sourceSpan.start + (isFirst ? 1 : 0); const raw = this.text.slice(start, end); - return this.#create( + return createNode( { type: 'TemplateElement', - value: { - cooked: node.text, - raw, - }, - start: start, - end: end, + value: { cooked: node.text, raw }, tail: isLast, }, - ancestors, + [start, end], ); } if (node instanceof angular.ParenthesizedExpression) { - return this.transform(node.expression, childTransformOptions); + return transformChild(node.expression); } /* c8 ignore next @preserve */ diff --git a/src/transform-template-binding.ts b/src/transform-template-binding.ts index 19ce3200..8a9c6004 100644 --- a/src/transform-template-binding.ts +++ b/src/transform-template-binding.ts @@ -54,9 +54,10 @@ class TemplateBindingTransformer extends NodeTransformer { } #create( - properties: Partial & { type: T['type'] } & RawNGSpan, + properties: Partial & { type: T['type'] }, + location: angular.AST | RawNGSpan | [number, number], ) { - return this.createNode(properties); + return this.createNode(properties, location); } #transform(node: angular.AST) { @@ -150,21 +151,17 @@ class TemplateBindingTransformer extends NodeTransformer { templateBinding.value && templateBinding.value.source === lastTemplateBinding.key.source ) { - const alias = this.#create({ - type: 'NGMicrosyntaxKey', - name: templateBinding.key.source, - ...templateBinding.key.span, - }); + const alias = this.#create( + { type: 'NGMicrosyntaxKey', name: templateBinding.key.source }, + templateBinding.key.span, + ); const updateSpanEnd = (node: T, end: number): T => ({ ...node, ...this.transformSpan({ start: node.start!, end }), }); const updateExpressionAlias = ( expression: NGMicrosyntaxExpression, - ) => ({ - ...updateSpanEnd(expression, alias.end), - alias, - }); + ) => ({ ...updateSpanEnd(expression, alias.end), alias }); const lastNode = body.pop()!; @@ -184,13 +181,12 @@ class TemplateBindingTransformer extends NodeTransformer { lastTemplateBinding = templateBinding; } - return this.#create({ - type: 'NGMicrosyntax', - body, - ...(body.length === 0 + return this.#create( + { type: 'NGMicrosyntax', body }, + body.length === 0 ? rawTemplateBindings[0].sourceSpan - : { start: body[0].start, end: body.at(-1)!.end }), - }); + : { start: body[0].start, end: body.at(-1)!.end }, + ); } #transformTemplateBinding( @@ -200,35 +196,41 @@ class TemplateBindingTransformer extends NodeTransformer { if (isExpressionBinding(templateBinding)) { const { key, value } = templateBinding; if (!value) { - return this.#create({ - type: 'NGMicrosyntaxKey', - name: this.#removePrefix(key.source), - ...key.span, - }); + return this.#create( + { type: 'NGMicrosyntaxKey', name: this.#removePrefix(key.source) }, + key.span, + ); } else if (index === 0) { - return this.#create({ - type: 'NGMicrosyntaxExpression', - expression: this.#transform(value.ast), - alias: null, - ...value.sourceSpan, - }); - } else { - return this.#create({ - type: 'NGMicrosyntaxKeyedExpression', - key: this.#create({ - type: 'NGMicrosyntaxKey', - name: this.#removePrefix(key.source), - ...key.span, - }), - expression: this.#create({ + return this.#create( + { type: 'NGMicrosyntaxExpression', expression: this.#transform(value.ast), alias: null, - ...value.sourceSpan, - }), - start: key.span.start, - end: value.sourceSpan.end, - }); + }, + value, + ); + } else { + return this.#create( + { + type: 'NGMicrosyntaxKeyedExpression', + key: this.#create( + { + type: 'NGMicrosyntaxKey', + name: this.#removePrefix(key.source), + }, + key.span, + ), + expression: this.#create( + { + type: 'NGMicrosyntaxExpression', + expression: this.#transform(value.ast), + alias: null, + }, + value, + ), + }, + [key.span.start, value.sourceSpan.end], + ); } } else { const { key, sourceSpan } = templateBinding; @@ -237,40 +239,38 @@ class TemplateBindingTransformer extends NodeTransformer { ); if (startsWithLet) { const { value } = templateBinding; - return this.#create({ - type: 'NGMicrosyntaxLet', - key: this.#create({ - type: 'NGMicrosyntaxKey', - name: key.source, - ...key.span, - }), - value: !value - ? null - : this.#create({ - type: 'NGMicrosyntaxKey', - name: value.source, - ...value.span, - }), - start: sourceSpan.start, - end: value ? value.span.end : key.span.end, - }); + return this.#create( + { + type: 'NGMicrosyntaxLet', + key: this.#create( + { type: 'NGMicrosyntaxKey', name: key.source }, + key.span, + ), + value: !value + ? null + : this.#create( + { type: 'NGMicrosyntaxKey', name: value.source }, + value.span, + ), + }, + [sourceSpan.start, value ? value.span.end : key.span.end], + ); } else { const value = this.#getAsVariableBindingValue(templateBinding); - return this.#create({ - type: 'NGMicrosyntaxAs', - key: this.#create({ - type: 'NGMicrosyntaxKey', - name: value!.source, - ...value!.span, - }), - alias: this.#create({ - type: 'NGMicrosyntaxKey', - name: key.source, - ...key.span, - }), - start: value!.span.start, - end: key.span.end, - }); + return this.#create( + { + type: 'NGMicrosyntaxAs', + key: this.#create( + { type: 'NGMicrosyntaxKey', name: value!.source }, + value!.span, + ), + alias: this.#create( + { type: 'NGMicrosyntaxKey', name: key.source }, + key.span, + ), + }, + [value!.span.start, key.span.end], + ); } } }