2022-11-08 15:37:30 +08:00

387 lines
11 KiB
TypeScript

import * as parser from '@babel/parser';
import generate from '@babel/generator';
import traverse, { NodePath } from '@babel/traverse';
import * as t from '@babel/types';
import { isIdentifier, Node } from '@babel/types';
import { OrderedSet } from './OrderedSet';
import { IScope } from '../types';
export class ParseError extends Error {
readonly expr: string | t.Expression;
readonly detail: unknown;
constructor(expr: string | t.Expression, detail: unknown) {
super(`Failed to parse expression "${typeof expr === 'string' ? expr : generate(expr)}"`);
this.expr = expr;
this.detail = detail;
Object.setPrototypeOf(this, new.target.prototype);
}
}
const MAYBE_EXPRESSIONS: {
[k in Node['type']]?: {
// fields: Array<keyof (Node & { type: k })>
fields: string[] | ((node: Node) => string[]);
};
} = {
ArrayExpression: { fields: ['elements'] },
AssignmentExpression: { fields: ['left', 'right'] },
BinaryExpression: { fields: ['left', 'right'] },
CallExpression: { fields: ['arguments', 'callee'] },
ConditionalExpression: { fields: ['test', 'consequent', 'alternate'] },
DoWhileStatement: { fields: ['test'] },
ExpressionStatement: { fields: ['expression'] },
ForInStatement: { fields: ['right'] },
ForStatement: { fields: ['init', 'test', 'update'] },
IfStatement: { fields: ['test'] },
LogicalExpression: { fields: ['left', 'right'] },
MemberExpression: {
fields: (node) => {
return node.type === 'MemberExpression' && node.computed
? ['object', 'property']
: ['object'];
},
},
NewExpression: { fields: ['callee', 'arguments'] },
ObjectMethod: {
fields: (node) => {
return node.type === 'ObjectMethod' && node.computed ? ['key'] : [];
},
},
ObjectProperty: {
fields: (node) => {
return node.type === 'ObjectProperty' && node.computed ? ['key', 'value'] : ['value'];
},
},
ReturnStatement: { fields: ['argument'] },
SequenceExpression: { fields: ['expressions'] },
ParenthesizedExpression: { fields: ['expression'] },
SwitchCase: { fields: ['test'] },
SwitchStatement: { fields: ['discriminant'] },
ThrowStatement: { fields: ['argument'] },
UnaryExpression: { fields: ['argument'] },
UpdateExpression: { fields: ['argument'] },
VariableDeclarator: { fields: ['init'] },
WhileStatement: { fields: ['test'] },
WithStatement: { fields: ['object'] },
AssignmentPattern: { fields: ['right'] },
ArrowFunctionExpression: { fields: ['body'] },
ClassExpression: { fields: ['superClass'] },
ClassDeclaration: { fields: ['superClass'] },
ExportDefaultDeclaration: { fields: ['declaration'] },
ForOfStatement: { fields: ['right'] },
ClassMethod: {
fields: (node) => {
return node.type === 'ClassMethod' && node.computed ? ['key'] : [];
},
},
SpreadElement: { fields: ['argument'] },
TaggedTemplateExpression: { fields: ['tag'] },
TemplateLiteral: { fields: ['expressions'] },
YieldExpression: { fields: ['argument'] },
AwaitExpression: { fields: ['argument'] },
OptionalMemberExpression: {
fields: (node) => {
return node.type === 'OptionalMemberExpression' && node.computed
? ['object', 'property']
: ['object'];
},
},
OptionalCallExpression: { fields: ['callee', 'arguments'] },
JSXSpreadAttribute: { fields: ['argument'] },
BindExpression: { fields: ['object', 'callee'] },
ClassProperty: {
fields: (node) => {
return node.type === 'ClassProperty' && node.computed ? ['key', 'value'] : ['value'];
},
},
PipelineTopicExpression: { fields: ['expression'] },
PipelineBareFunction: { fields: ['callee'] },
ClassPrivateProperty: { fields: ['value'] },
Decorator: { fields: ['expression'] },
TupleExpression: { fields: ['elements'] },
TSDeclareMethod: {
fields: (node) => {
return node.type === 'TSDeclareMethod' && node.computed ? ['key'] : [];
},
},
TSPropertySignature: {
fields: (node) => {
return node.type === 'TSPropertySignature' && node.computed
? ['key', 'initializer']
: ['initializer'];
},
},
TSMethodSignature: {
fields: (node) => {
return node.type === 'TSMethodSignature' && node.computed ? ['key'] : [];
},
},
TSAsExpression: { fields: ['expression'] },
TSTypeAssertion: { fields: ['expression'] },
TSEnumDeclaration: { fields: ['initializer'] },
TSEnumMember: { fields: ['initializer'] },
TSNonNullExpression: { fields: ['expression'] },
TSExportAssignment: { fields: ['expression'] },
};
export interface ParseExpressionGetGlobalVariablesOptions {
filter?: (varName: string) => boolean;
}
const CROSS_THIS_SCOPE_TYPE_NODE: {
[k in Node['type']]?: boolean;
} = {
ArrowFunctionExpression: false, // 箭头函数不跨越 this 的 scope
FunctionExpression: true,
FunctionDeclaration: true,
// FunctionTypeAnnotation: false, // 这是 TS 定义
// FunctionTypeParam: false, // 这是 TS 定义
ClassDeclaration: true,
ClassExpression: true,
ClassBody: true,
ClassImplements: true,
ClassMethod: true,
ClassPrivateMethod: true,
ClassProperty: true,
ClassPrivateProperty: true,
DeclareClass: true,
};
const JS_KEYWORDS = ['arguments', 'this', 'super'];
export function parseExpressionGetKeywords(expr: string | null | undefined): string[] {
if (!expr) {
return [];
}
try {
const keywordVars = new OrderedSet<string>();
const ast = parser.parse(`!(${expr});`, {
plugins: [
'jsx',
],
});
const addIdentifierIfNeeded = (x: Record<string, unknown> | number | null | undefined) => {
if (typeof x === 'object' && isIdentifier(x) && JS_KEYWORDS.includes(x.name)) {
keywordVars.add(x.name);
}
};
traverse(ast, {
enter(path) {
const { node } = path;
const expressionFields = MAYBE_EXPRESSIONS[node.type]?.fields;
if (expressionFields) {
(typeof expressionFields === 'function'
? expressionFields(node)
: expressionFields
).forEach((fieldName) => {
const fieldValue = node[fieldName as keyof typeof node];
if (typeof fieldValue === 'object') {
if (Array.isArray(fieldValue)) {
fieldValue.forEach((item: any) => {
addIdentifierIfNeeded(item);
});
} else {
addIdentifierIfNeeded(fieldValue as Record<string, unknown> | null);
}
}
});
}
},
});
return keywordVars.toArray().filter(Boolean);
} catch (e) {
throw new ParseError(expr, e);
}
}
export function parseExpressionGetGlobalVariables(
expr: string | null | undefined,
{ filter = () => true }: ParseExpressionGetGlobalVariablesOptions = {},
): string[] {
if (!expr) {
return [];
}
try {
const undeclaredVars = new OrderedSet<string>();
const ast = parser.parse(`!(${expr});`);
const addUndeclaredIdentifierIfNeeded = (
x: Record<string, unknown> | number | null | undefined,
path: NodePath<Node>,
) => {
if (typeof x === 'object' && isIdentifier(x) && !path.scope.hasBinding(x.name)) {
undeclaredVars.add(x.name);
}
};
traverse(ast, {
enter(path) {
const { node } = path;
const expressionFields = MAYBE_EXPRESSIONS[node.type]?.fields;
if (expressionFields) {
(typeof expressionFields === 'function'
? expressionFields(node)
: expressionFields
).forEach((fieldName) => {
const fieldValue = node[fieldName as keyof typeof node];
if (typeof fieldValue === 'object') {
if (Array.isArray(fieldValue)) {
fieldValue.forEach((item: any) => {
addUndeclaredIdentifierIfNeeded(item, path);
});
} else {
addUndeclaredIdentifierIfNeeded(fieldValue as Record<string, unknown> | null, path);
}
}
});
}
},
});
return undeclaredVars.toArray().filter(filter);
} catch (e) {
throw new ParseError(expr, e);
}
}
export function parseExpressionConvertThis2Context(
expr: string | t.Expression,
contextName = '__$$context',
localVariables: string[] = [],
): string {
if (!expr) {
return expr;
}
try {
const exprAst = typeof expr === 'string' ? parser.parseExpression(expr) : expr;
const exprWrapAst = t.expressionStatement(exprAst);
const fileAst = t.file(t.program([exprWrapAst]));
const localVariablesSet = new Set(localVariables);
let thisScopeLevel = CROSS_THIS_SCOPE_TYPE_NODE[exprAst.type] ? -1 : 0;
traverse(fileAst, {
enter(path) {
if (CROSS_THIS_SCOPE_TYPE_NODE[path.node.type]) {
thisScopeLevel++;
}
},
exit(path) {
if (CROSS_THIS_SCOPE_TYPE_NODE[path.node.type]) {
thisScopeLevel--;
}
},
MemberExpression(path) {
if (!path.isMemberExpression()) {
return;
}
const obj = path.get('object');
if (!obj.isThisExpression()) {
return;
}
// 处理局部变量
if (!path.node.computed) {
const prop = path.get('property');
if (prop.isIdentifier() && localVariablesSet.has(prop.node.name)) {
path.replaceWith(t.identifier(prop.node.name));
return;
}
}
// 替换 this (只在顶层替换)
if (thisScopeLevel <= 0) {
obj.replaceWith(t.identifier(contextName));
}
},
ThisExpression(path) {
if (!path.isThisExpression()) {
return;
}
// MemberExpression 中的 this.xxx 已经处理过了
if (path.parent.type === 'MemberExpression') {
return;
}
if (thisScopeLevel <= 0) {
path.replaceWith(t.identifier(contextName));
}
},
});
const { code } = generate(exprWrapAst.expression, { sourceMaps: false });
return code;
} catch (e) {
throw new ParseError(expr, e);
}
}
export function parseExpression(expr: string) {
try {
return parser.parseExpression(expr);
} catch (e) {
throw new ParseError(expr, e);
}
}
export function transformExpressionLocalRef(expr: string, scope: IScope) {
if (!expr) {
return expr;
}
if (!scope) {
throw new Error('transform expression without scope');
}
try {
const exprAst = typeof expr === 'string' ? parser.parseExpression(expr) : expr;
const exprWrapAst = t.expressionStatement(exprAst);
const fileAst = t.file(t.program([exprWrapAst]));
traverse(fileAst, {
MemberExpression(path) {
if (!path.isMemberExpression()) {
return;
}
const obj = path.get('object');
if (!obj.isThisExpression()) {
return;
}
// 查看是否存在引用 local 值
const prop = path.get('property');
let memberName = '';
if (!path.node.computed && prop.isIdentifier()) {
memberName = prop.node.name;
} else if (path.node.computed && prop.isStringLiteral()) {
memberName = prop.node.value;
}
if (memberName && scope.bindings && scope.bindings.hasBinding(memberName)) {
path.replaceWith(t.identifier(memberName));
}
},
});
const { code } = generate(exprWrapAst.expression, { sourceMaps: false });
return code;
} catch (e) {
throw new ParseError(expr, e);
// throw e;
}
}