UNPKG

angular-estree-parser

Version:

A parser that converts Angular source code into an ESTree-compatible form

374 lines (373 loc) 15.6 kB
import * as angular from '@angular/compiler'; import Source from './source.js'; function isParenthesized(node) { return Boolean(node.extra?.parenthesized); } function getOuterStart(node) { return isParenthesized(node) ? node.extra.parenStart : node.start; } function getOuterEnd(node) { return isParenthesized(node) ? node.extra.parenEnd : node.end; } function isOptionalObjectOrCallee(node) { return ((node.type === 'OptionalCallExpression' || node.type === 'OptionalMemberExpression') && !isParenthesized(node)); } function isImplicitThis(node, text) { const { start, end } = node.sourceSpan; return start >= end || /^\s+$/.test(text.slice(start, end)); } class Transformer extends Source { #node; #text; constructor(ast, text) { super(text); this.#node = ast; this.#text = text; } get node() { return this.#transform(this.#node); } transformNode(node) { return this.#transformNode(node); } #create(properties, { stripSpaces = true, hasParentParens = false } = {}) { return this.createNode(properties, { stripSpaces, hasParentParens }); } #transformReceiverAndName(receiver, property, { computed, optional, end = getOuterEnd(property), hasParentParens = false, }) { if (isImplicitThis(receiver, this.#text) || receiver.sourceSpan.start === property.start) { return property; } const object = this.#transform(receiver); const isOptionalObject = isOptionalObjectOrCallee(object); return this.#create({ type: optional || isOptionalObject ? 'OptionalMemberExpression' : 'MemberExpression', object, property, computed: computed, ...(optional ? { optional: true } : isOptionalObject ? { optional: false } : undefined), start: getOuterStart(object), end, }, { hasParentParens }); } #transform(node, isInParentParens = false) { return this.#transformNode(node, isInParentParens); } #transformNode(node, isInParentParens = false) { if (node instanceof angular.Interpolation) { const { expressions } = node; // istanbul ignore next 3 if (expressions.length !== 1) { throw new Error("Unexpected 'Interpolation'"); } return this.#transform(expressions[0]); } if (node instanceof angular.Unary) { return this.#create({ type: 'UnaryExpression', prefix: true, argument: this.#transform(node.expr), operator: node.operator, ...node.sourceSpan, }, { hasParentParens: isInParentParens }); } 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, }; if (operator === '&&' || operator === '||' || operator === '??') { return this.#create({ ...properties, type: 'LogicalExpression', operator: operator, }, { hasParentParens: isInParentParens }); } return this.#create({ ...properties, type: 'BinaryExpression', operator: operator, }, { hasParentParens: isInParentParens }); } 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(/\S/, this.getCharacterIndex('|', leftEnd) + 1); const right = this.#create({ type: 'Identifier', name, start: rightStart, end: rightStart + name.length, }); const argumentNodes = originalArguments.map((node) => this.#transform(node)); 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)), }, { hasParentParens: isInParentParens }); } if (node instanceof angular.Chain) { return this.#create({ type: 'NGChainedExpression', expressions: node.expressions.map((node) => this.#transform(node)), ...node.sourceSpan, }, { hasParentParens: isInParentParens }); } 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); return this.#create({ type: 'ConditionalExpression', test, consequent, alternate, start: getOuterStart(test), end: getOuterEnd(alternate), }, { hasParentParens: isInParentParens }); } if (node instanceof angular.EmptyExpr) { return this.#create({ type: 'NGEmptyExpression', ...node.sourceSpan }, { hasParentParens: isInParentParens }); } if (node instanceof angular.ImplicitReceiver) { return this.#create({ type: 'ThisExpression', ...node.sourceSpan }, { hasParentParens: isInParentParens }); } if (node instanceof angular.KeyedRead || node instanceof angular.SafeKeyedRead) { return this.#transformReceiverAndName(node.receiver, this.#transform(node.key), { computed: true, optional: node instanceof angular.SafeKeyedRead, end: node.sourceSpan.end, // ] hasParentParens: isInParentParens, }); } if (node instanceof angular.LiteralArray) { return this.#create({ type: 'ArrayExpression', elements: node.expressions.map((node) => this.#transform(node)), ...node.sourceSpan, }, { hasParentParens: isInParentParens }); } 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 keyStart = this.getCharacterIndex(/\S/, index === 0 ? node.sourceSpan.start + 1 // { : this.getCharacterIndex(',', getOuterEnd(tValues[index - 1])) + 1); const keyEnd = valueStart === keyStart ? valueEnd : this.getCharacterLastIndex(/\S/, this.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, }); const shorthand = tKey.end < tKey.start || keyStart === valueStart; return this.#create({ type: 'ObjectProperty', key: tKey, value: tValue, shorthand, computed: false, start: getOuterStart(tKey), end: valueEnd, }); }); return this.#create({ type: 'ObjectExpression', properties: tProperties, ...node.sourceSpan, }, { hasParentParens: isInParentParens }); } if (node instanceof angular.LiteralPrimitive) { const { value } = node; switch (typeof value) { case 'boolean': return this.#create({ type: 'BooleanLiteral', value, ...node.sourceSpan }, { hasParentParens: isInParentParens }); case 'number': return this.#create({ type: 'NumericLiteral', value, ...node.sourceSpan }, { hasParentParens: isInParentParens }); case 'object': return this.#create({ type: 'NullLiteral', ...node.sourceSpan }, { hasParentParens: isInParentParens }); case 'string': return this.#create({ type: 'StringLiteral', value, ...node.sourceSpan }, { hasParentParens: isInParentParens }); case 'undefined': return this.#create({ type: 'Identifier', name: 'undefined', ...node.sourceSpan }, { hasParentParens: isInParentParens }); // istanbul ignore next default: throw new Error(`Unexpected LiteralPrimitive value type ${typeof value}`); } } 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], true)] : args.map((node) => this.#transform(node)); const tReceiver = this.#transform(receiver); const isOptionalReceiver = isOptionalObjectOrCallee(tReceiver); const nodeType = isOptionalType || isOptionalReceiver ? 'OptionalCallExpression' : 'CallExpression'; return this.#create({ type: nodeType, callee: tReceiver, arguments: tArgs, optional: nodeType === 'OptionalCallExpression' ? isOptionalType : undefined, start: getOuterStart(tReceiver), end: node.sourceSpan.end, // ) }, { hasParentParens: isInParentParens }); } if (node instanceof angular.NonNullAssert) { const expression = this.#transform(node.expression); return this.#create({ type: 'TSNonNullExpression', expression: expression, start: getOuterStart(expression), end: node.sourceSpan.end, // ! }, { hasParentParens: isInParentParens }); } const isPrefixNot = node instanceof angular.PrefixNot; if (isPrefixNot || node instanceof angular.TypeofExpression) { const expression = this.#transform(node.expression); const operator = isPrefixNot ? '!' : 'typeof'; let { start } = node.sourceSpan; if (!isPrefixNot) { const index = this.text.lastIndexOf(operator, start); // istanbul ignore next 7 if (index === -1) { throw new Error(`Cannot find operator ${operator} from index ${start} in ${JSON.stringify(this.text)}`); } start = index; } return this.#create({ type: 'UnaryExpression', prefix: true, operator, argument: expression, start, end: getOuterEnd(expression), }, { hasParentParens: isInParentParens }); } if (node instanceof angular.PropertyRead || node instanceof angular.SafePropertyRead) { const { receiver, name } = node; const nameEnd = this.getCharacterLastIndex(/\S/, node.sourceSpan.end - 1) + 1; const tName = this.#create({ type: 'Identifier', name, start: nameEnd - name.length, end: nameEnd, }, isImplicitThis(receiver, this.#text) ? { hasParentParens: isInParentParens } : {}); return this.#transformReceiverAndName(receiver, tName, { computed: false, optional: node instanceof angular.SafePropertyRead, hasParentParens: isInParentParens, }); } if (node instanceof angular.KeyedWrite) { const key = this.#transform(node.key); const right = this.#transform(node.value); const left = this.#transformReceiverAndName(node.receiver, key, { computed: true, optional: false, end: this.getCharacterIndex(']', getOuterEnd(key)) + 1, }); return this.#create({ type: 'AssignmentExpression', left: left, operator: '=', right, start: getOuterStart(left), end: getOuterEnd(right), }, { hasParentParens: isInParentParens }); } if (node instanceof angular.PropertyWrite) { const { receiver, name, value } = node; const tValue = this.#transform(value); const nameEnd = this.getCharacterLastIndex(/\S/, this.getCharacterLastIndex('=', getOuterStart(tValue) - 1) - 1) + 1; const tName = this.#create({ type: 'Identifier', name, start: nameEnd - name.length, end: nameEnd, }); const tReceiverAndName = this.#transformReceiverAndName(receiver, tName, { computed: false, optional: false, }); return this.#create({ type: 'AssignmentExpression', left: tReceiverAndName, operator: '=', right: tValue, start: getOuterStart(tReceiverAndName), end: getOuterEnd(tValue), }, { hasParentParens: isInParentParens }); } // istanbul ignore next throw Object.assign(new Error('Unexpected node'), { node }); } } // See `convertAst` in `@angular/compiler` // ASTWithSource (Not handled) // PropertyRead // PropertyWrite // KeyedWrite // Call // LiteralPrimitive // Unary // Binary // ThisReceiver (Not handled) // KeyedRead // Chain // LiteralMap // LiteralArray // Conditional // NonNullAssert // BindingPipe // SafeKeyedRead // SafePropertyRead // SafeCall // EmptyExpr // PrefixNot // TypeofExpression function transform(node, text) { return new Transformer(node, text).node; } export { transform, Transformer };