diff --git a/.gitignore b/.gitignore index 1ab1310c..0e524cb2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ coverage/ -dist/ +dist*/ node_modules/ *.log .yarn/* diff --git a/src/__snapshots__/utils.test.ts.snap b/src/__snapshots__/utils.test.ts.snap deleted file mode 100644 index ebfc80ba..00000000 --- a/src/__snapshots__/utils.test.ts.snap +++ /dev/null @@ -1,85 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`fitSpans > start: 0, end: 15 1`] = ` -origin -> 1 | ( ( ( 1 ) ) ) - | ^^^^^^^^^^^^^^^ -inner -> 1 | ( ( ( 1 ) ) ) - | ^^^^^^^^^^^^^ -outer hasParens=false -> 1 | ( ( ( 1 ) ) ) - | ^^^^^^^^^^^^^ -`; - -exports[`fitSpans > start: 1, end: 14 1`] = ` -origin -> 1 | ( ( ( 1 ) ) ) - | ^^^^^^^^^^^^^ -inner -> 1 | ( ( ( 1 ) ) ) - | ^^^^^^^^^^^^^ -outer hasParens=false -> 1 | ( ( ( 1 ) ) ) - | ^^^^^^^^^^^^^ -`; - -exports[`fitSpans > start: 2, end: 13 1`] = ` -origin -> 1 | ( ( ( 1 ) ) ) - | ^^^^^^^^^^^ -inner -> 1 | ( ( ( 1 ) ) ) - | ^^^^^^^^^ -outer hasParens=true -> 1 | ( ( ( 1 ) ) ) - | ^^^^^^^^^^^^^ -`; - -exports[`fitSpans > start: 3, end: 12 1`] = ` -origin -> 1 | ( ( ( 1 ) ) ) - | ^^^^^^^^^ -inner -> 1 | ( ( ( 1 ) ) ) - | ^^^^^^^^^ -outer hasParens=true -> 1 | ( ( ( 1 ) ) ) - | ^^^^^^^^^^^^^ -`; - -exports[`fitSpans > start: 4, end: 11 1`] = ` -origin -> 1 | ( ( ( 1 ) ) ) - | ^^^^^^^ -inner -> 1 | ( ( ( 1 ) ) ) - | ^^^^^ -outer hasParens=true -> 1 | ( ( ( 1 ) ) ) - | ^^^^^^^^^^^^^ -`; - -exports[`fitSpans > start: 5, end: 10 1`] = ` -origin -> 1 | ( ( ( 1 ) ) ) - | ^^^^^ -inner -> 1 | ( ( ( 1 ) ) ) - | ^^^^^ -outer hasParens=true -> 1 | ( ( ( 1 ) ) ) - | ^^^^^^^^^^^^^ -`; - -exports[`fitSpans > start: 6, end: 9 1`] = ` -origin -> 1 | ( ( ( 1 ) ) ) - | ^^^ -inner -> 1 | ( ( ( 1 ) ) ) - | ^ -outer hasParens=true -> 1 | ( ( ( 1 ) ) ) - | ^^^^^^^^^^^^^ -`; diff --git a/src/source.ts b/src/source.ts index e639fc36..c79701ba 100644 --- a/src/source.ts +++ b/src/source.ts @@ -2,7 +2,6 @@ import type * as babel from '@babel/types'; import type { LocationInformation, NGNode, RawNGSpan } from './types.ts'; import { - fitSpans, getCharacterIndex, getCharacterLastIndex, sourceSpanToLocationInformation, @@ -23,49 +22,19 @@ export class Source { return getCharacterLastIndex(this.text, pattern, index); } - transformSpan( - span: RawNGSpan, - { stripSpaces = false, hasParentParens = false } = {}, - ): LocationInformation { - if (!stripSpaces) { - return sourceSpanToLocationInformation(span); - } - - const { outerSpan, innerSpan, hasParens } = fitSpans( - span, - this.text, - hasParentParens, - ); - const locationInformation = sourceSpanToLocationInformation(innerSpan); - if (hasParens) { - locationInformation.extra = { - parenthesized: true, - parenStart: outerSpan.start, - parenEnd: outerSpan.end, - }; - } - - return locationInformation; + transformSpan(span: RawNGSpan): LocationInformation { + return sourceSpanToLocationInformation(span); } createNode( properties: Partial & { type: T['type'] } & RawNGSpan, - // istanbul ignore next - { stripSpaces = true, hasParentParens = false } = {}, ) { - const { type, start, end } = properties; const node = { ...properties, - ...this.transformSpan( - { start, end }, - { - stripSpaces, - hasParentParens, - }, - ), + range: [properties.start, properties.end], } as T & LocationInformation; - switch (type) { + switch (node.type) { case 'NumericLiteral': case 'StringLiteral': case 'RegExpLiteral': { diff --git a/src/transform-node.ts b/src/transform-node.ts index e852f541..26ad0493 100644 --- a/src/transform-node.ts +++ b/src/transform-node.ts @@ -14,12 +14,6 @@ import type { function isParenthesized(node: NGNode) { return Boolean(node.extra?.parenthesized); } -function getOuterStart(node: NGNode): number { - return isParenthesized(node) ? node.extra.parenStart : node.start!; -} -function getOuterEnd(node: NGNode): number { - return isParenthesized(node) ? node.extra.parenEnd : node.end!; -} function isOptionalObjectOrCallee(node: NGNode): boolean { if (node.type === 'TSNonNullExpression' && !isParenthesized(node)) { @@ -33,130 +27,55 @@ function isOptionalObjectOrCallee(node: NGNode): boolean { ); } -function isImplicitThis(node: angular.AST, text: string): boolean { - const { start, end } = node.sourceSpan; - return start >= end || /^\s+$/.test(text.slice(start, end)); -} - type NodeTransformOptions = { - isInParentParens?: boolean; - parent?: angular.AST; + ancestors: angular.AST[]; }; class Transformer extends Source { - #node; - #text; - - constructor(ast: angular.AST | undefined, text: string) { + constructor(text: string) { super(text); - this.#node = ast; - this.#text = text; - } - - get node() { - return this.#transform(this.#node!); } - transformNode(node: angular.AST) { - return this.#transformNode(node) as T & LocationInformation; + transform( + node: angular.AST, + options?: NodeTransformOptions, + ) { + return this.#transform(node, options ?? { ancestors: [] }) as T & + LocationInformation; } #create( properties: Partial & { type: T['type'] } & RawNGSpan, - { stripSpaces = true, hasParentParens = false } = {}, + ancestors: angular.AST[], ) { - return this.createNode(properties, { stripSpaces, hasParentParens }); - } - - #transformReceiverAndName( - node: - | angular.KeyedRead - | angular.SafeKeyedRead - | angular.PropertyRead - | angular.SafePropertyRead, - property: babel.Expression, - { - computed, - optional, - hasParentParens = false, - }: { - computed: boolean; - optional: boolean; - hasParentParens?: boolean; - }, - ) { - const { receiver } = node; - if ( - isImplicitThis(receiver, this.#text) || - receiver.sourceSpan.start === property.start - ) { - return property; - } - const object = this.#transform(receiver); - const isOptionalObject = isOptionalObjectOrCallee(object); - - const commonProps = { - property, - object, - computed, - ...node.sourceSpan, - }; - - if (optional || isOptionalObject) { - return this.#create( - { - type: 'OptionalMemberExpression', - optional: optional || !isOptionalObject, - ...commonProps, - }, - { hasParentParens }, - ); - } + const node = super.createNode(properties); - if (computed) { - return this.#create( - { - type: 'MemberExpression', - ...commonProps, - computed: true, - }, - { hasParentParens }, - ); + if (ancestors[0] instanceof angular.ParenthesizedExpression) { + node.extra = { + ...node.extra, + parenthesized: true, + }; } - return this.#create( - { - type: 'MemberExpression', - ...commonProps, - computed: false, - property: property as babel.MemberExpressionNonComputed['property'], - }, - { hasParentParens }, - ); + return node; } - #transform( - node: angular.AST, - options?: NodeTransformOptions, - ) { - return this.#transformNode(node, options) as T & LocationInformation; - } - - #transformNode(node: angular.AST, options?: NodeTransformOptions): NGNode { - const { isInParentParens } = { - isInParentParens: false, + #transform(node: angular.AST, options: NodeTransformOptions): NGNode { + const ancestors = options.ancestors; + const childTransformOptions = { ...options, + ancestors: [node, ...ancestors], }; if (node instanceof angular.Interpolation) { const { expressions } = node; - /* c8 ignore next 3 */ + /* c8 ignore next 3 @preserve */ if (expressions.length !== 1) { throw new Error("Unexpected 'Interpolation'"); } - return this.#transform(expressions[0]); + return this.transform(expressions[0], childTransformOptions); } if (node instanceof angular.Unary) { @@ -164,91 +83,90 @@ class Transformer extends Source { { type: 'UnaryExpression', prefix: true, - argument: this.#transform(node.expr), + argument: this.transform(node.expr), operator: node.operator as '-' | '+', ...node.sourceSpan, }, - { hasParentParens: isInParentParens }, + ancestors, ); } if (node instanceof angular.Binary) { - const { - left: originalLeft, - operation: operator, - right: originalRight, - } = node; - const left = this.#transform(originalLeft); - const right = this.#transform(originalRight); - const start = getOuterStart(left); - const end = getOuterEnd(right); - const properties = { left, right, start, end }; + const { operation: operator } = node; + const [left, right] = [node.left, node.right].map((node) => + this.transform(node, childTransformOptions), + ); if (operator === '&&' || operator === '||' || operator === '??') { return this.#create( { - ...properties, type: 'LogicalExpression', operator: operator as babel.LogicalExpression['operator'], + left, + right, + ...node.sourceSpan, }, - { hasParentParens: isInParentParens }, + ancestors, ); } if (angular.Binary.isAssignmentOperation(operator)) { return this.#create( { - ...properties, type: 'AssignmentExpression', left: left as babel.MemberExpression, + right, operator: operator as babel.AssignmentExpression['operator'], ...node.sourceSpan, }, - { hasParentParens: isInParentParens }, + ancestors, ); } return this.#create( { - ...properties, + left, + right, type: 'BinaryExpression', operator: operator as babel.BinaryExpression['operator'], + ...node.sourceSpan, }, - { hasParentParens: isInParentParens }, + ancestors, ); } if (node instanceof angular.BindingPipe) { - const { exp: expressionNode, name, args: originalArguments } = node; - const left = this.#transform(expressionNode); - const start = getOuterStart(left); - const leftEnd = getOuterEnd(left); - const rightStart = this.getCharacterIndex( + const { name } = node; + const left = this.transform( + node.exp, + childTransformOptions, + ); + const leftEnd = node.exp.sourceSpan.end; + const rightStart = super.getCharacterIndex( /\S/, - this.getCharacterIndex('|', leftEnd) + 1, + super.getCharacterIndex('|', leftEnd) + 1, ); - const right = this.#create({ - type: 'Identifier', - name, - start: rightStart, - end: rightStart + name.length, - }); - const argumentNodes = originalArguments.map((node) => - this.#transform(node), + 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: argumentNodes, - start, - end: getOuterEnd( - // TODO[@fisker]: End seems not correct, since there should be `()` - argumentNodes.length === 0 ? right : argumentNodes.at(-1)!, - ), + arguments: arguments_, + ...node.sourceSpan, }, - { hasParentParens: isInParentParens }, + ancestors, ); } @@ -257,43 +175,46 @@ class Transformer extends Source { { type: 'NGChainedExpression', expressions: node.expressions.map((node) => - this.#transform(node), + this.transform(node, childTransformOptions), ), ...node.sourceSpan, }, - { hasParentParens: isInParentParens }, + ancestors, ); } if (node instanceof angular.Conditional) { - const { condition, trueExp, falseExp } = node; - const test = this.#transform(condition); - const consequent = this.#transform(trueExp); - const alternate = this.#transform(falseExp); + const [test, consequent, alternate] = [ + node.condition, + node.trueExp, + node.falseExp, + ].map((node) => + this.transform(node, childTransformOptions), + ); + return this.#create( { type: 'ConditionalExpression', test, consequent, alternate, - start: getOuterStart(test), - end: getOuterEnd(alternate), + ...node.sourceSpan, }, - { hasParentParens: isInParentParens }, + ancestors, ); } if (node instanceof angular.EmptyExpr) { return this.#create( { type: 'NGEmptyExpression', ...node.sourceSpan }, - { hasParentParens: isInParentParens }, + ancestors, ); } if (node instanceof angular.ImplicitReceiver) { return this.#create( { type: 'ThisExpression', ...node.sourceSpan }, - { hasParentParens: isInParentParens }, + ancestors, ); } @@ -302,60 +223,70 @@ class Transformer extends Source { { type: 'ArrayExpression', elements: node.expressions.map((node) => - this.#transform(node), + this.transform(node, childTransformOptions), ), ...node.sourceSpan, }, - { hasParentParens: isInParentParens }, + ancestors, ); } if (node instanceof angular.LiteralMap) { const { keys, values } = node; - const tValues = values.map((value) => - this.#transform(value), - ); - const tProperties = keys.map(({ key, quoted }, index) => { - const tValue = tValues[index]; - const valueStart = getOuterStart(tValue); - const valueEnd = getOuterEnd(tValue); + const tProperties = keys.map((property, index) => { + const { key, quoted } = property; + const { start: valueStart, end: valueEnd } = values[index].sourceSpan; - const keyStart = this.getCharacterIndex( + const keyStart = super.getCharacterIndex( /\S/, index === 0 ? node.sourceSpan.start + 1 // { - : this.getCharacterIndex(',', getOuterEnd(tValues[index - 1])) + 1, + : super.getCharacterIndex(',', values[index - 1].sourceSpan.end) + + 1, ); const keyEnd = valueStart === keyStart ? valueEnd - : this.getCharacterLastIndex( + : super.getCharacterLastIndex( /\S/, - this.getCharacterLastIndex(':', valueStart - 1) - 1, + super.getCharacterLastIndex(':', valueStart - 1) - 1, ) + 1; const keySpan = { start: keyStart, end: keyEnd }; const tKey = quoted - ? this.#create({ - type: 'StringLiteral', - value: key, - ...keySpan, - }) - : this.#create({ - type: 'Identifier', - name: key, - ...keySpan, - }); + ? this.#create( + { + type: 'StringLiteral', + value: key, + ...keySpan, + }, + [], + ) + : this.#create( + { + type: 'Identifier', + name: key, + ...keySpan, + }, + [], + ); const shorthand = tKey.end < tKey.start || keyStart === valueStart; + const value = this.transform( + values[index], + childTransformOptions, + ); - return this.#create({ - type: 'ObjectProperty', - key: tKey, - value: tValue, - shorthand, - computed: false, - start: getOuterStart(tKey), - end: valueEnd, - }); + return this.#create( + { + type: 'ObjectProperty', + key: tKey, + value, + shorthand, + computed: false, + start: tKey.start, + end: valueEnd, + }, + [], + ); }); return this.#create( { @@ -363,7 +294,7 @@ class Transformer extends Source { properties: tProperties, ...node.sourceSpan, }, - { hasParentParens: isInParentParens }, + ancestors, ); } @@ -373,27 +304,27 @@ class Transformer extends Source { case 'boolean': return this.#create( { type: 'BooleanLiteral', value, ...node.sourceSpan }, - { hasParentParens: isInParentParens }, + ancestors, ); case 'number': return this.#create( { type: 'NumericLiteral', value, ...node.sourceSpan }, - { hasParentParens: isInParentParens }, + ancestors, ); case 'object': return this.#create( { type: 'NullLiteral', ...node.sourceSpan }, - { hasParentParens: isInParentParens }, + ancestors, ); case 'string': return this.#create( { type: 'StringLiteral', value, ...node.sourceSpan }, - { hasParentParens: isInParentParens }, + ancestors, ); case 'undefined': return this.#create( { type: 'Identifier', name: 'undefined', ...node.sourceSpan }, - { hasParentParens: isInParentParens }, + ancestors, ); /* c8 ignore next 4 */ default: @@ -411,54 +342,44 @@ class Transformer extends Source { flags: node.flags ?? '', ...node.sourceSpan, }, - { hasParentParens: isInParentParens }, + ancestors, ); } if (node instanceof angular.Call || node instanceof angular.SafeCall) { - const isOptionalType = node instanceof angular.SafeCall; - const { receiver, args } = node; - const tArgs = - args.length === 1 - ? [ - this.#transform(args[0], { - isInParentParens: true, - }), - ] - : (args as angular.AST[]).map((node) => - this.#transform(node), - ); - const tReceiver = this.#transform(receiver!); - const isOptionalReceiver = isOptionalObjectOrCallee(tReceiver); + const arguments_ = node.args.map((node) => + this.transform(node, childTransformOptions), + ); + const callee = this.transform(node.receiver); + const isOptionalReceiver = isOptionalObjectOrCallee(callee); + const isOptional = node instanceof angular.SafeCall; const nodeType = - isOptionalType || isOptionalReceiver + isOptional || isOptionalReceiver ? 'OptionalCallExpression' : 'CallExpression'; return this.#create( { type: nodeType, - callee: tReceiver, - arguments: tArgs, + callee, + arguments: arguments_, ...(nodeType === 'OptionalCallExpression' - ? { optional: isOptionalType } + ? { optional: isOptional } : undefined), - start: getOuterStart(tReceiver), - end: node.sourceSpan.end, // `)` + ...node.sourceSpan, }, - { hasParentParens: isInParentParens }, + ancestors, ); } if (node instanceof angular.NonNullAssert) { - const expression = this.#transform(node.expression); + const expression = this.transform(node.expression); return this.#create( { type: 'TSNonNullExpression', expression: expression, - start: getOuterStart(expression), - end: node.sourceSpan.end, // `!` + ...node.sourceSpan, }, - { hasParentParens: isInParentParens }, + ancestors, ); } @@ -474,10 +395,10 @@ class Transformer extends Source { ? 'typeof' : node instanceof angular.VoidExpression ? 'void' - : /* c8 ignore next */ + : /* c8 ignore next @preserve */ undefined; - /* c8 ignore next 3 */ + /* c8 ignore next 3 @preserve */ if (!operator) { throw new Error('Unexpected expression.'); } @@ -487,7 +408,7 @@ class Transformer extends Source { if (operator === 'typeof' || operator === 'void') { const index = this.text.lastIndexOf(operator, start); - /* c8 ignore next 7 */ + /* c8 ignore next 7 @preserve */ if (index === -1) { throw new Error( `Cannot find operator '${operator}' from index ${start} in ${JSON.stringify( @@ -499,7 +420,7 @@ class Transformer extends Source { start = index; } - const expression = this.#transform(node.expression); + const expression = this.transform(node.expression); return this.#create( { @@ -508,75 +429,125 @@ class Transformer extends Source { operator, argument: expression, start, - end: getOuterEnd(expression), + end: node.sourceSpan.end, }, - { hasParentParens: isInParentParens }, + ancestors, ); } if ( node instanceof angular.KeyedRead || - node instanceof angular.SafeKeyedRead - ) { - return this.#transformReceiverAndName( - node, - this.#transform(node.key), - { - computed: true, - optional: node instanceof angular.SafeKeyedRead, - hasParentParens: isInParentParens, - }, - ); - } - - if ( + node instanceof angular.SafeKeyedRead || node instanceof angular.PropertyRead || node instanceof angular.SafePropertyRead ) { - const { receiver, name } = node; - const tName = this.#create( + const isComputed = + node instanceof angular.KeyedRead || + node instanceof angular.SafeKeyedRead; + const isOptional = + node instanceof angular.SafeKeyedRead || + node instanceof angular.SafePropertyRead; + + const { receiver } = node; + + let isImplicitThis; + + let property; + if (isComputed) { + isImplicitThis = node.sourceSpan.start === node.key.sourceSpan.start; + property = this.transform(node.key); + } else { + const { name, nameSpan } = node; + + isImplicitThis = node.sourceSpan.start === nameSpan.start; + property = this.#create( + { + type: 'Identifier', + name, + ...node.nameSpan, + }, + isImplicitThis ? ancestors : [], + ); + } + + if (isImplicitThis) { + return property; + } + + const object = this.transform(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, + ); + } + + if (isComputed) { + return this.#create( + { + type: 'MemberExpression', + ...commonProps, + computed: true, + }, + ancestors, + ); + } + + return this.#create( { - type: 'Identifier', - name, - ...node.nameSpan, + type: 'MemberExpression', + ...commonProps, + computed: false, + property: property as babel.MemberExpressionNonComputed['property'], }, - isImplicitThis(receiver, this.#text) - ? { hasParentParens: isInParentParens } - : {}, + ancestors, ); - return this.#transformReceiverAndName(node, tName, { - computed: false, - optional: node instanceof angular.SafePropertyRead, - hasParentParens: isInParentParens, - }); } if (node instanceof angular.TaggedTemplateLiteral) { - return this.#create({ - type: 'TaggedTemplateExpression', - tag: this.#transform(node.tag) as babel.Expression, - quasi: this.#transform(node.template) as babel.TemplateLiteral, - ...node.sourceSpan, - }); + return this.#create( + { + type: 'TaggedTemplateExpression', + tag: this.transform(node.tag), + quasi: this.transform(node.template), + ...node.sourceSpan, + }, + ancestors, + ); } if (node instanceof angular.TemplateLiteral) { - const { elements, expressions } = node; - - return this.#create({ - type: 'TemplateLiteral', - quasis: elements.map((element) => - this.#transform(element, { parent: node }), - ), - expressions: expressions.map((expression) => - this.#transform(expression), - ), - ...node.sourceSpan, - }); + 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, + ); } if (node instanceof angular.TemplateLiteralElement) { - const { elements } = options!.parent! as angular.TemplateLiteral; + const [parent] = ancestors; + const { elements } = parent as angular.TemplateLiteral; const elementIndex = elements.indexOf(node); const isFirst = elementIndex === 0; const isLast = elementIndex === elements.length - 1; @@ -596,15 +567,15 @@ class Transformer extends Source { end: end, tail: isLast, }, - { stripSpaces: false }, + ancestors, ); } if (node instanceof angular.ParenthesizedExpression) { - return this.#transformNode(node.expression); + return this.transform(node.expression, childTransformOptions); } - /* c8 ignore next */ + /* c8 ignore next @preserve */ throw new Error(`Unexpected node type '${node.constructor.name}'`); } } @@ -636,7 +607,7 @@ type SupportedNodes = | angular.TaggedTemplateLiteral | angular.ParenthesizedExpression; function transform(node: SupportedNodes, text: string): NGNode { - return new Transformer(node, text).node; + return new Transformer(text).transform(node); } export { transform, Transformer }; diff --git a/src/transform-template-binding.ts b/src/transform-template-binding.ts index b0e89acd..19ce3200 100644 --- a/src/transform-template-binding.ts +++ b/src/transform-template-binding.ts @@ -30,12 +30,12 @@ function isVariableBinding( return templateBinding instanceof NGVariableBinding; } -class Transformer extends NodeTransformer { +class TemplateBindingTransformer extends NodeTransformer { #rawTemplateBindings; #text; constructor(rawTemplateBindings: angular.TemplateBinding[], text: string) { - super(undefined, text); + super(text); this.#rawTemplateBindings = rawTemplateBindings; this.#text = text; @@ -55,13 +55,12 @@ class Transformer extends NodeTransformer { #create( properties: Partial & { type: T['type'] } & RawNGSpan, - { stripSpaces = true } = {}, ) { - return this.createNode(properties, { stripSpaces }); + return this.createNode(properties); } #transform(node: angular.AST) { - return this.transformNode(node) as T; + return super.transform(node) as T; } #removePrefix(string: string) { @@ -175,7 +174,7 @@ class Transformer extends NodeTransformer { const expression = updateExpressionAlias(lastNode.expression); body.push(updateSpanEnd({ ...lastNode, expression }, expression.end)); } else { - /* c8 ignore next 2 */ + /* c8 ignore next 2 @preserve */ throw new Error(`Unexpected type ${lastNode.type}`); } } else { @@ -281,7 +280,7 @@ function transform( rawTemplateBindings: angular.TemplateBinding[], text: string, ) { - return new Transformer(rawTemplateBindings, text).expressions; + return new TemplateBindingTransformer(rawTemplateBindings, text).expressions; } export { transform }; diff --git a/src/utils.test.ts b/src/utils.test.ts index ca171a1f..abf4ce0f 100644 --- a/src/utils.test.ts +++ b/src/utils.test.ts @@ -1,39 +1,4 @@ -import { codeFrameColumns } from '@babel/code-frame'; -import { wrap } from 'jest-snapshot-serializer-raw'; - -import type { RawNGSpan } from './types.ts'; -import { fitSpans, getCharacterIndex, getCharacterLastIndex } from './utils.ts'; - -const text = ` ( ( ( 1 ) ) ) `; -const length = Math.floor(text.length / 2); - -describe('fitSpans', () => { - for (let i = 0; i < length; i++) { - const start = i; - const end = text.length - i; - test(`start: ${start}, end: ${end}`, () => { - const { innerSpan, outerSpan, hasParens } = fitSpans( - { start, end }, - text, - false, - ); - const show = ({ start: startColumn, end: endColumn }: RawNGSpan) => - codeFrameColumns(text, { - start: { line: 1, column: startColumn + 1 }, - end: { line: 1, column: endColumn + 1 }, - }); - const snapshot = [ - 'origin', - show({ start, end }), - 'inner', - show(innerSpan), - `outer hasParens=${hasParens}`, - show(outerSpan), - ].join('\n'); - expect(wrap(snapshot)).toMatchSnapshot(); - }); - } -}); +import { getCharacterIndex, getCharacterLastIndex } from './utils.ts'; test('getCharacterIndex', () => { expect(getCharacterIndex('foobar', /o/, 0)).toBe(1); diff --git a/src/utils.ts b/src/utils.ts index 9c327e0e..2d4f37dd 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,88 +1,5 @@ import type { LocationInformation, RawNGSpan } from './types.ts'; -function stripSurroundingSpaces( - { start: startIndex, end: endIndex }: RawNGSpan, - text: string, -) { - let start = startIndex; - let end = endIndex; - - while (end !== start && /\s/.test(text[end - 1])) { - end--; - } - - while (start !== end && /\s/.test(text[start])) { - start++; - } - - return { start, end }; -} - -function expandSurroundingSpaces( - { start: startIndex, end: endIndex }: RawNGSpan, - text: string, -) { - let start = startIndex; - let end = endIndex; - - while (end !== text.length && /\s/.test(text[end])) { - end++; - } - - while (start !== 0 && /\s/.test(text[start - 1])) { - start--; - } - - return { start, end }; -} - -function expandSurroundingParens(span: RawNGSpan, text: string) { - return text[span.start - 1] === '(' && text[span.end] === ')' - ? { start: span.start - 1, end: span.end + 1 } - : span; -} - -export function fitSpans( - span: RawNGSpan, - text: string, - hasParentParens: boolean, -): { outerSpan: RawNGSpan; innerSpan: RawNGSpan; hasParens: boolean } { - let parensCount = 0; - - const outerSpan = { start: span.start, end: span.end }; - - while (true) { - const spacesExpandedSpan = expandSurroundingSpaces(outerSpan, text); - const parensExpandedSpan = expandSurroundingParens( - spacesExpandedSpan, - text, - ); - - if ( - spacesExpandedSpan.start === parensExpandedSpan.start && - spacesExpandedSpan.end === parensExpandedSpan.end - ) { - break; - } - - outerSpan.start = parensExpandedSpan.start; - outerSpan.end = parensExpandedSpan.end; - - parensCount++; - } - - return { - hasParens: (hasParentParens ? parensCount - 1 : parensCount) !== 0, - outerSpan: stripSurroundingSpaces( - hasParentParens - ? { start: outerSpan.start + 1, end: outerSpan.end - 1 } - : outerSpan, - text, - ), - innerSpan: stripSurroundingSpaces(span, text), - }; -} - function getCharacterSearchTestFunction(pattern: RegExp | string) { if (typeof pattern === 'string') { return (character: string) => character === pattern; @@ -106,7 +23,7 @@ export function getCharacterLastIndex( } } - /* c8 ignore next 4 */ + /* c8 ignore next 4 @preserve */ throw new Error( `Cannot find front char ${pattern} from index ${fromIndex} in ${JSON.stringify( text, @@ -129,6 +46,7 @@ export function getCharacterIndex( } } + /* c8 ignore next 4 @preserve */ throw new Error( `Cannot find character ${pattern} from index ${fromIndex} in ${JSON.stringify( text, diff --git a/tests/__snapshots__/transform.test.ts.snap b/tests/__snapshots__/transform.test.ts.snap index 47486d8d..9c95cf2b 100644 --- a/tests/__snapshots__/transform.test.ts.snap +++ b/tests/__snapshots__/transform.test.ts.snap @@ -1,26 +1,35 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`('BindingPipe' -> 'NGPipeExpression') > parseBinding(" a | b ") 1`] = ` +exports[`('BindingPipe' -> 'NGPipeExpression') > parseBinding(" ( ( ( ( a ) ) | b )) ") 1`] = ` NGPipeExpression { "left": "Identifier", "right": "Identifier", "arguments": [], + "extra": { + "parenthesized": true + }, "comments": [] } -> 1 | a | b - | ^^^^^ +> 1 | ( ( ( ( a ) ) | b )) + | ^^^^^^^^^^^^^^ -------------------------------------------------------------------------------- Identifier { - "name": "a" + "name": "a", + "extra": { + "parenthesized": true + } } -> 1 | a | b - | ^ +> 1 | ( ( ( ( a ) ) | b )) + | ^ -------------------------------------------------------------------------------- Identifier { - "name": "b" + "name": "b", + "extra": { + "parenthesized": true + } } -> 1 | a | b - | ^ +> 1 | ( ( ( ( a ) ) | b )) + | ^ `; exports[`('BindingPipe' -> 'NGPipeExpression') > parseBinding(" a | b : c ") 1`] = ` @@ -54,27 +63,36 @@ Identifier { | ^ `; -exports[`('BindingPipe' -> 'NGPipeExpression') > parseInterpolationExpression(" a | b ") 1`] = ` +exports[`('BindingPipe' -> 'NGPipeExpression') > parseInterpolationExpression(" ( ( ( ( a ) ) | b )) ") 1`] = ` NGPipeExpression { "left": "Identifier", "right": "Identifier", "arguments": [], + "extra": { + "parenthesized": true + }, "comments": [] } -> 1 | a | b - | ^^^^^ +> 1 | ( ( ( ( a ) ) | b )) + | ^^^^^^^^^^^^^^ -------------------------------------------------------------------------------- Identifier { - "name": "a" + "name": "a", + "extra": { + "parenthesized": true + } } -> 1 | a | b - | ^ +> 1 | ( ( ( ( a ) ) | b )) + | ^ -------------------------------------------------------------------------------- Identifier { - "name": "b" + "name": "b", + "extra": { + "parenthesized": true + } } -> 1 | a | b - | ^ +> 1 | ( ( ( ( a ) ) | b )) + | ^ `; exports[`('BindingPipe' -> 'NGPipeExpression') > parseInterpolationExpression(" a | b : c ") 1`] = ` diff --git a/tests/helpers.ts b/tests/helpers.ts index 2ff7e7b6..0017e6a2 100644 --- a/tests/helpers.ts +++ b/tests/helpers.ts @@ -46,6 +46,10 @@ export function massageAst(ast: any, parser: 'babel' | 'angular'): any { return ast.map((node) => massageAst(node, parser)); } + if (parser === 'babel' && typeof ast.extra?.parenStart === 'number') { + delete ast.extra.parenStart; + } + // Not exists in types, but exists in node. if (ast.type === 'ObjectProperty') { if (ast.method !== undefined && ast.method !== false) { @@ -54,6 +58,7 @@ export function massageAst(ast: any, parser: 'babel' | 'angular'): any { ); } delete ast.method; + if ( ast.shorthand && ast.extra && diff --git a/tests/transform.test.ts b/tests/transform.test.ts index 13334d56..2073e85c 100644 --- a/tests/transform.test.ts +++ b/tests/transform.test.ts @@ -27,23 +27,28 @@ const PARSE_METHODS = [ describe.each` expectedAngularType | expectedEstreeType | text | parseAction | parseBinding | parseSimpleBinding | parseInterpolationExpression - ${'Binary'} | ${'BinaryExpression'} | ${' 0 - 1 '} | ${true} | ${true} | ${true} | ${true} - ${'Binary'} | ${'BinaryExpression'} | ${' a ** b '} | ${true} | ${true} | ${true} | ${true} + ${'Binary'} | ${'BinaryExpression'} | ${' ( ( ( ( 0 ) ) - ( ( 1 ) ) ) ) '} | ${true} | ${true} | ${true} | ${true} + ${'Binary'} | ${'BinaryExpression'} | ${' ( ( ( ( a ) ) ** ( ( b ) ) ) ) '} | ${true} | ${true} | ${true} | ${true} ${'Binary'} | ${'BinaryExpression'} | ${' ( ( ( ( a ) ) in ( ( b ) ) ) ) '} | ${true} | ${true} | ${true} | ${true} - ${'Binary'} | ${'LogicalExpression'} | ${' a && b '} | ${true} | ${true} | ${true} | ${true} - ${'Binary'} | ${'LogicalExpression'} | ${' a ?? b '} | ${true} | ${true} | ${true} | ${true} + ${'Binary'} | ${'LogicalExpression'} | ${' ( ( ( ( a ) ) && ( ( b ) ) ) ) '} | ${true} | ${true} | ${true} | ${true} + ${'Binary'} | ${'LogicalExpression'} | ${' ( ( ( ( a ) ) || ( ( b ) ) ) ) '} | ${true} | ${true} | ${true} | ${true} + ${'Binary'} | ${'LogicalExpression'} | ${' ( ( ( ( a ) ) ?? ( ( b ) ) ) ) '} | ${true} | ${true} | ${true} | ${true} + ${'Binary'} | ${'AssignmentExpression'} | ${' ( ( a . b = ( ( 1 ) ) ) ) '} | ${true} | ${false} | ${false} | ${false} + ${'Binary'} | ${'AssignmentExpression'} | ${' ( ( a = ( ( 1 ) ) ) ) '} | ${true} | ${false} | ${false} | ${false} + ${'Binary'} | ${'AssignmentExpression'} | ${' a [ b ] = 1 '} | ${true} | ${true} | ${true} | ${true} + ${'Binary'} | ${'AssignmentExpression'} | ${' ( ( a ??= ( ( 1 ) ) ) ) '} | ${true} | ${false} | ${false} | ${false} ${'Unary'} | ${'UnaryExpression'} | ${' - 1 '} | ${true} | ${true} | ${true} | ${true} ${'Unary'} | ${'UnaryExpression'} | ${' + 1 '} | ${true} | ${true} | ${true} | ${true} - ${'BindingPipe'} | ${'NGPipeExpression'} | ${' a | b '} | ${false} | ${true} | ${false} | ${true} + ${'BindingPipe'} | ${'NGPipeExpression'} | ${' ( ( ( ( a ) ) | b )) '} | ${false} | ${true} | ${false} | ${true} ${'BindingPipe'} | ${'NGPipeExpression'} | ${' a | b : c '} | ${false} | ${true} | ${false} | ${true} ${'Chain'} | ${'NGChainedExpression'} | ${' a ; b '} | ${true} | ${false} | ${false} | ${false} - ${'Conditional'} | ${'ConditionalExpression'} | ${' a ? 1 : 2 '} | ${true} | ${true} | ${true} | ${true} + ${'Conditional'} | ${'ConditionalExpression'} | ${' ( ( ( ( a ) ) ? ( ( 1 ) ) : ( ( 2 ) ) ))'} | ${true} | ${true} | ${true} | ${true} ${'EmptyExpr'} | ${'NGEmptyExpression'} | ${''} | ${true} | ${true} | ${true} | ${true} - ${'Call'} | ${'CallExpression'} | ${' ( a . b ) ( 1 , 2 ) '} | ${true} | ${true} | ${true} | ${true} - ${'SafeCall'} | ${'OptionalCallExpression'} | ${' ( a . b )?.( 1 , 2 ) '} | ${true} | ${true} | ${true} | ${true} - ${'Call'} | ${'CallExpression'} | ${' ( a ) ( 1 , 2 ) '} | ${true} | ${true} | ${true} | ${true} - ${'SafeCall'} | ${'OptionalCallExpression'} | ${' ( a )?.( 1 , 2 ) '} | ${true} | ${true} | ${true} | ${true} - ${'Call'} | ${'CallExpression'} | ${' a ( 1 ) ( 2 ) '} | ${true} | ${true} | ${true} | ${true} + ${'Call'} | ${'CallExpression'} | ${' ( ( ( ( a . b ) ) ( 1 , 2 ) ) ) '} | ${true} | ${true} | ${true} | ${true} + ${'SafeCall'} | ${'OptionalCallExpression'} | ${' ( ( ( ( a . b ) )?.( 1 , 2 ) ) ) '} | ${true} | ${true} | ${true} | ${true} + ${'Call'} | ${'CallExpression'} | ${' ( ( ( ( a ) ) ( 1 , 2 ) ) ) '} | ${true} | ${true} | ${true} | ${true} + ${'SafeCall'} | ${'OptionalCallExpression'} | ${' ( ( ( ( a ) ) ?. ( 1 , 2 ) ) ) '} | ${true} | ${true} | ${true} | ${true} + ${'Call'} | ${'CallExpression'} | ${' ( ( a ( ( ( 1 ) ) ) ( ( ( 1 ) ) ) ) ) '} | ${true} | ${true} | ${true} | ${true} ${'SafeCall'} | ${'OptionalCallExpression'} | ${' a ( 1 )?.( 2 ) '} | ${true} | ${true} | ${true} | ${true} ${'KeyedRead'} | ${'MemberExpression'} | ${' a [ b ] '} | ${true} | ${true} | ${true} | ${true} ${'SafeKeyedRead'} | ${'OptionalMemberExpression'} | ${' a ?. [ b ] '} | ${true} | ${true} | ${true} | ${true} @@ -51,10 +56,9 @@ describe.each` ${'SafeKeyedRead'} | ${'OptionalMemberExpression'} | ${' a ?. b ?. [ c ] '} | ${true} | ${true} | ${true} | ${true} ${'KeyedRead'} | ${'OptionalMemberExpression'} | ${' a ?. b () [ c ] '} | ${true} | ${true} | ${true} | ${true} ${'SafeKeyedRead'} | ${'OptionalMemberExpression'} | ${' a ?. b () ?. [ c ] '} | ${true} | ${true} | ${true} | ${true} - ${'Binary'} | ${'AssignmentExpression'} | ${' a [ b ] = 1 '} | ${true} | ${true} | ${true} | ${true} ${'ImplicitReceiver'} | ${'ThisExpression'} | ${' this '} | ${true} | ${true} | ${true} | ${true} ${'LiteralArray'} | ${'ArrayExpression'} | ${' [ 1 ] '} | ${true} | ${true} | ${true} | ${true} - ${'LiteralMap'} | ${'ObjectExpression'} | ${' ( { "a" : 1 } )'} | ${true} | ${true} | ${true} | ${true} + ${'LiteralMap'} | ${'ObjectExpression'} | ${' ( ( { "a" : ( ( 1 ) ) } ) )'} | ${true} | ${true} | ${true} | ${true} ${'LiteralMap'} | ${'ObjectExpression'} | ${' ( { a : 1 } ) '} | ${true} | ${true} | ${true} | ${true} ${'Call'} | ${'CallExpression'} | ${' f ( { a : 1 } ) '} | ${true} | ${true} | ${true} | ${true} ${'LiteralMap'} | ${'ObjectExpression'} | ${' ( {a, b: 2} ) '} | ${true} | ${true} | ${true} | ${true} @@ -81,8 +85,7 @@ describe.each` ${'SafeCall'} | ${'OptionalCallExpression'} | ${' a ?. b . c ?. ( ) '} | ${true} | ${true} | ${true} | ${true} ${'Call'} | ${'OptionalCallExpression'} | ${' a ?. b ( ) . c ( ) '} | ${true} | ${true} | ${true} | ${true} ${'SafeCall'} | ${'OptionalCallExpression'} | ${' a ?. b ( ) . c ?.( ) '} | ${true} | ${true} | ${true} | ${true} - ${'NonNullAssert'} | ${'TSNonNullExpression'} | ${' x ! '} | ${true} | ${true} | ${true} | ${true} - ${'PrefixNot'} | ${'UnaryExpression'} | ${' ! x '} | ${true} | ${true} | ${true} | ${true} + ${'NonNullAssert'} | ${'TSNonNullExpression'} | ${' ( ( ( ( x ) ) ! ) ) '} | ${true} | ${true} | ${true} | ${true} ${'PropertyRead'} | ${'Identifier'} | ${' ( ( a ) ) '} | ${true} | ${true} | ${true} | ${true} ${'PropertyRead'} | ${'Identifier'} | ${' a '} | ${true} | ${true} | ${true} | ${true} ${'PropertyRead'} | ${'Identifier'} | ${' a // hello '} | ${true} | ${true} | ${true} | ${true} @@ -93,15 +96,13 @@ describe.each` ${'PropertyRead'} | ${'OptionalMemberExpression'} | ${' foo?.bar!.bam '} | ${true} | ${true} | ${true} | ${true} ${'PropertyRead'} | ${'MemberExpression'} | ${' (foo?.bar)!.bam '} | ${true} | ${true} | ${true} | ${true} ${'PropertyRead'} | ${'MemberExpression'} | ${' (foo?.bar!).bam '} | ${true} | ${true} | ${true} | ${true} - ${'Binary'} | ${'AssignmentExpression'} | ${' a . b = 1 '} | ${true} | ${false} | ${false} | ${false} - ${'Binary'} | ${'AssignmentExpression'} | ${' a = 1 '} | ${true} | ${false} | ${false} | ${false} ${'Call'} | ${'OptionalCallExpression'} | ${' a ?. b ( ) '} | ${true} | ${true} | ${true} | ${true} ${'SafeCall'} | ${'OptionalCallExpression'} | ${' a ?. b ?. ( ) '} | ${true} | ${true} | ${true} | ${true} ${'SafePropertyRead'} | ${'OptionalMemberExpression'} | ${' a ?. b '} | ${true} | ${true} | ${true} | ${true} - ${'TypeofExpression'} | ${'UnaryExpression'} | ${' ( ( typeof {} ) ) '} | ${true} | ${true} | ${true} | ${true} + ${'PrefixNot'} | ${'UnaryExpression'} | ${' ( ( ! ( ( x ) ) ) ) '} | ${true} | ${true} | ${true} | ${true} + ${'TypeofExpression'} | ${'UnaryExpression'} | ${' ( ( typeof ( ( x ) ) ) ) '} | ${true} | ${true} | ${true} | ${true} + ${'VoidExpression'} | ${'UnaryExpression'} | ${' ( ( void ( ( x ) ) ) ) '} | ${true} | ${true} | ${true} | ${true} ${'Binary'} | ${'BinaryExpression'} | ${' typeof {} === "object" '} | ${true} | ${true} | ${true} | ${true} - ${'PrefixNot'} | ${'UnaryExpression'} | ${' ! ( typeof {} === "" ) '} | ${true} | ${true} | ${true} | ${true} - ${'VoidExpression'} | ${'UnaryExpression'} | ${' ( ( void ( ( a() ) ) ) ) '} | ${true} | ${true} | ${true} | ${true} ${'TemplateLiteral'} | ${'TemplateLiteral'} | ${' ` a ${ b } \\u0063 ` '} | ${true} | ${true} | ${true} | ${true} ${'TemplateLiteral'} | ${'TemplateLiteral'} | ${' ( ( ` a ${ b } \\u0063 ` ) ) '} | ${true} | ${true} | ${true} | ${true} ${'TemplateLiteral'} | ${'TemplateLiteral'} | ${' ` \\u0063 ` '} | ${true} | ${true} | ${true} | ${true} @@ -111,7 +112,6 @@ describe.each` ${'TaggedTemplateLiteral'} | ${'TaggedTemplateExpression'} | ${' ( ( ( ( tag ) ) ` a ${ b } \\u0063 ` ) ) '} | ${true} | ${true} | ${true} | ${true} ${'LiteralMap'} | ${'ObjectExpression'} | ${' ( ( {foo: ` a ${ b } ` } ) ) '} | ${true} | ${true} | ${true} | ${true} ${'LiteralMap'} | ${'ObjectExpression'} | ${' ( ( {foo: tag ` a ${ b } ` } ) ) '} | ${true} | ${true} | ${true} | ${true} - ${'Binary'} | ${'AssignmentExpression'} | ${' a ??= b '} | ${true} | ${false} | ${false} | ${false} `('($expectedAngularType -> $expectedEstreeType)', (fields) => { for (const method of PARSE_METHODS) { testSection(method, fields);