423 lines
11 KiB
Plaintext
423 lines
11 KiB
Plaintext
/**
|
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*
|
|
* @flow strict
|
|
* @format
|
|
*/
|
|
|
|
import type {HermesNode} from './HermesAST';
|
|
import type {ParserOptions} from './ParserOptions';
|
|
|
|
import HermesASTAdapter from './HermesASTAdapter';
|
|
import {getModuleDocblock} from './getModuleDocblock';
|
|
|
|
declare var BigInt: ?(value: $FlowFixMe) => mixed;
|
|
|
|
export default class HermesToESTreeAdapter extends HermesASTAdapter {
|
|
+code: string;
|
|
|
|
constructor(options: ParserOptions, code: string) {
|
|
super(options);
|
|
this.code = code;
|
|
}
|
|
|
|
fixSourceLocation(node: HermesNode): void {
|
|
const loc = node.loc;
|
|
if (loc == null) {
|
|
return;
|
|
}
|
|
|
|
node.loc = {
|
|
source: this.sourceFilename ?? null,
|
|
start: loc.start,
|
|
end: loc.end,
|
|
};
|
|
|
|
node.range = [loc.rangeStart, loc.rangeEnd];
|
|
|
|
delete node.start;
|
|
delete node.end;
|
|
}
|
|
|
|
mapNode(node: HermesNode): HermesNode {
|
|
this.fixSourceLocation(node);
|
|
switch (node.type) {
|
|
case 'Program':
|
|
return this.mapProgram(node);
|
|
case 'NullLiteral':
|
|
return this.mapNullLiteral(node);
|
|
case 'BooleanLiteral':
|
|
case 'StringLiteral':
|
|
case 'NumericLiteral':
|
|
case 'JSXStringLiteral':
|
|
return this.mapSimpleLiteral(node);
|
|
case 'BigIntLiteral':
|
|
return this.mapBigIntLiteral(node);
|
|
case 'RegExpLiteral':
|
|
return this.mapRegExpLiteral(node);
|
|
case 'Empty':
|
|
return this.mapEmpty(node);
|
|
case 'TemplateElement':
|
|
return this.mapTemplateElement(node);
|
|
case 'BigIntLiteralTypeAnnotation':
|
|
return this.mapBigIntLiteralTypeAnnotation(node);
|
|
case 'GenericTypeAnnotation':
|
|
return this.mapGenericTypeAnnotation(node);
|
|
case 'ImportDeclaration':
|
|
return this.mapImportDeclaration(node);
|
|
case 'ImportSpecifier':
|
|
return this.mapImportSpecifier(node);
|
|
case 'ExportDefaultDeclaration':
|
|
return this.mapExportDefaultDeclaration(node);
|
|
case 'ExportNamedDeclaration':
|
|
return this.mapExportNamedDeclaration(node);
|
|
case 'ExportAllDeclaration':
|
|
return this.mapExportAllDeclaration(node);
|
|
case 'Property':
|
|
return this.mapProperty(node);
|
|
case 'FunctionDeclaration':
|
|
case 'FunctionExpression':
|
|
case 'ArrowFunctionExpression':
|
|
return this.mapFunction(node);
|
|
case 'PrivateName':
|
|
return this.mapPrivateName(node);
|
|
case 'ClassProperty':
|
|
case 'ClassPrivateProperty':
|
|
return this.mapClassProperty(node);
|
|
case 'MemberExpression':
|
|
case 'OptionalMemberExpression':
|
|
case 'CallExpression':
|
|
case 'OptionalCallExpression':
|
|
return this.mapChainExpression(node);
|
|
default:
|
|
return this.mapNodeDefault(node);
|
|
}
|
|
}
|
|
|
|
mapProgram(node: HermesNode): HermesNode {
|
|
const nodeDefault = this.mapNodeDefault(node);
|
|
node.sourceType = this.getSourceType();
|
|
|
|
node.docblock = getModuleDocblock(nodeDefault);
|
|
|
|
return nodeDefault;
|
|
}
|
|
|
|
mapSimpleLiteral(node: HermesNode): HermesNode {
|
|
return {
|
|
type: 'Literal',
|
|
loc: node.loc,
|
|
range: node.range,
|
|
value: node.value,
|
|
raw: this.code.slice(node.range[0], node.range[1]),
|
|
literalType: (() => {
|
|
switch (node.type) {
|
|
case 'NullLiteral':
|
|
return 'null';
|
|
|
|
case 'BooleanLiteral':
|
|
return 'boolean';
|
|
|
|
case 'StringLiteral':
|
|
case 'JSXStringLiteral':
|
|
return 'string';
|
|
|
|
case 'NumericLiteral':
|
|
return 'numeric';
|
|
|
|
case 'BigIntLiteral':
|
|
return 'bigint';
|
|
|
|
case 'RegExpLiteral':
|
|
return 'regexp';
|
|
}
|
|
return null;
|
|
})(),
|
|
};
|
|
}
|
|
|
|
mapBigIntLiteral(node: HermesNode): HermesNode {
|
|
const newNode = this.mapSimpleLiteral(node);
|
|
return {
|
|
...newNode,
|
|
...this.getBigIntLiteralValue(node.bigint),
|
|
};
|
|
}
|
|
|
|
mapNullLiteral(node: HermesNode): HermesNode {
|
|
return {
|
|
...this.mapSimpleLiteral(node),
|
|
value: null,
|
|
};
|
|
}
|
|
|
|
mapRegExpLiteral(node: HermesNode): HermesNode {
|
|
const {pattern, flags} = node;
|
|
|
|
// Create RegExp value if possible. This can fail when the flags are invalid.
|
|
let value;
|
|
try {
|
|
value = new RegExp(pattern, flags);
|
|
} catch (e) {
|
|
value = null;
|
|
}
|
|
|
|
return {
|
|
...this.mapSimpleLiteral(node),
|
|
value,
|
|
regex: {
|
|
pattern,
|
|
flags,
|
|
},
|
|
};
|
|
}
|
|
|
|
mapBigIntLiteralTypeAnnotation(node: HermesNode): HermesNode {
|
|
return {
|
|
...node,
|
|
...this.getBigIntLiteralValue(node.raw),
|
|
};
|
|
}
|
|
|
|
mapTemplateElement(node: HermesNode): HermesNode {
|
|
return {
|
|
type: 'TemplateElement',
|
|
loc: node.loc,
|
|
range: node.range,
|
|
tail: node.tail,
|
|
value: {
|
|
cooked: node.cooked,
|
|
raw: node.raw,
|
|
},
|
|
};
|
|
}
|
|
|
|
mapGenericTypeAnnotation(node: HermesNode): HermesNode {
|
|
// Convert simple `this` generic type to ThisTypeAnnotation
|
|
if (
|
|
node.typeParameters == null &&
|
|
node.id.type === 'Identifier' &&
|
|
node.id.name === 'this'
|
|
) {
|
|
return {
|
|
type: 'ThisTypeAnnotation',
|
|
loc: node.loc,
|
|
range: node.range,
|
|
};
|
|
}
|
|
|
|
return this.mapNodeDefault(node);
|
|
}
|
|
|
|
mapProperty(nodeUnprocessed: HermesNode): HermesNode {
|
|
const node = this.mapNodeDefault(nodeUnprocessed);
|
|
|
|
if (node.value.type === 'FunctionExpression') {
|
|
node.value.loc.start = node.key.loc.end;
|
|
node.value.range[0] = node.key.range[1];
|
|
}
|
|
|
|
return node;
|
|
}
|
|
|
|
mapComment(node: HermesNode): HermesNode {
|
|
if (node.type === 'CommentBlock') {
|
|
node.type = 'Block';
|
|
} else if (node.type === 'CommentLine') {
|
|
node.type = 'Line';
|
|
}
|
|
|
|
return node;
|
|
}
|
|
|
|
mapFunction(nodeUnprocessed: HermesNode): HermesNode {
|
|
const node = this.mapNodeDefault(nodeUnprocessed);
|
|
|
|
switch (node.type) {
|
|
case 'FunctionDeclaration':
|
|
case 'FunctionExpression':
|
|
node.expression = false;
|
|
return node;
|
|
|
|
case 'ArrowFunctionExpression':
|
|
node.expression = node.body.type !== 'BlockStatement';
|
|
return node;
|
|
}
|
|
|
|
return node;
|
|
}
|
|
|
|
mapChainExpression(nodeUnprocessed: HermesNode): HermesNode {
|
|
/*
|
|
NOTE - In the below comments `MemberExpression` and `CallExpression`
|
|
are completely interchangable. For terseness we just reference
|
|
one each time.
|
|
*/
|
|
|
|
/*
|
|
Hermes uses the old babel-style AST:
|
|
```
|
|
(one?.two).three?.four;
|
|
^^^^^^^^^^^^^^^^^^^^^^ OptionalMemberExpression
|
|
^^^^^^^^^^^^^^^^ MemberExpression
|
|
^^^^^^^^ OptionalMemberExpression
|
|
```
|
|
|
|
We need to convert it to the ESTree representation:
|
|
```
|
|
(one?.two).three?.four;
|
|
^^^^^^^^^^^^^^^^^^^^^^ ChainExpression
|
|
^^^^^^^^^^^^^^^^^^^^^^ MemberExpression[optional = true]
|
|
^^^^^^^^^^^^^^^^ MemberExpression[optional = false]
|
|
^^^^^^^^ ChainExpression
|
|
^^^^^^^^ MemberExpression[optional = true]
|
|
```
|
|
|
|
We do this by converting the AST and its children (depth first), and then unwrapping
|
|
the resulting AST as appropriate.
|
|
|
|
Put another way:
|
|
1) traverse to the leaf
|
|
2) if the current node is an `OptionalMemberExpression`:
|
|
a) if the `.object` is a `ChainExpression`:
|
|
i) unwrap the child (`node.object = child.expression`)
|
|
b) convert this node to a `MemberExpression[optional = true]`
|
|
c) wrap this node (`node = ChainExpression[expression = node]`)
|
|
3) if the current node is a `MembedExpression`:
|
|
a) convert this node to a `MemberExpression[optional = true]`
|
|
*/
|
|
|
|
const node = this.mapNodeDefault(nodeUnprocessed);
|
|
|
|
const {child, childKey, isOptional} = ((): {
|
|
child: HermesNode,
|
|
childKey: string,
|
|
isOptional: boolean,
|
|
} => {
|
|
const isOptional: boolean = node.optional === true;
|
|
if (node.type.endsWith('MemberExpression')) {
|
|
return {
|
|
child: node.object,
|
|
childKey: 'object',
|
|
isOptional,
|
|
};
|
|
} else if (node.type.endsWith('CallExpression')) {
|
|
return {
|
|
child: node.callee,
|
|
childKey: 'callee',
|
|
isOptional,
|
|
};
|
|
} else {
|
|
return {
|
|
child: node.expression,
|
|
childKey: 'expression',
|
|
isOptional: false,
|
|
};
|
|
}
|
|
})();
|
|
|
|
const isChildUnwrappable =
|
|
child.type === 'ChainExpression' &&
|
|
// (x?.y).z is semantically different to `x?.y.z`.
|
|
// In the un-parenthesised case `.z` is only executed if and only if `x?.y` returns a non-nullish value.
|
|
// In the parenthesised case, `.z` is **always** executed, regardless of the return of `x?.y`.
|
|
// As such the AST is different between the two cases.
|
|
//
|
|
// In the hermes AST - any member part of a non-short-circuited optional chain is represented with `OptionalMemberExpression`
|
|
// so if we see a `MemberExpression`, then we know we've hit a parenthesis boundary.
|
|
node.type !== 'MemberExpression' &&
|
|
node.type !== 'CallExpression';
|
|
|
|
if (node.type.startsWith('Optional')) {
|
|
node.type = node.type.replace('Optional', '');
|
|
node.optional = isOptional;
|
|
} else {
|
|
node.optional = false;
|
|
}
|
|
|
|
if (!isChildUnwrappable && !isOptional) {
|
|
return node;
|
|
}
|
|
|
|
if (isChildUnwrappable) {
|
|
const newChild = child.expression;
|
|
node[childKey] = newChild;
|
|
}
|
|
|
|
return {
|
|
type: 'ChainExpression',
|
|
expression: node,
|
|
loc: node.loc,
|
|
range: node.range,
|
|
};
|
|
}
|
|
|
|
mapClassProperty(nodeUnprocessed: HermesNode): HermesNode {
|
|
const node = this.mapNodeDefault(nodeUnprocessed);
|
|
|
|
const key = (() => {
|
|
if (node.type === 'ClassPrivateProperty') {
|
|
const key = this.mapNodeDefault(node.key);
|
|
return {
|
|
type: 'PrivateIdentifier',
|
|
name: key.name,
|
|
range: key.range,
|
|
loc: key.loc,
|
|
};
|
|
}
|
|
|
|
return node.key;
|
|
})();
|
|
|
|
return {
|
|
...node,
|
|
computed: node.type === 'ClassPrivateProperty' ? false : node.computed,
|
|
key,
|
|
type: 'PropertyDefinition',
|
|
};
|
|
}
|
|
|
|
mapPrivateName(node: HermesNode): HermesNode {
|
|
return {
|
|
type: 'PrivateIdentifier',
|
|
name: node.id.name,
|
|
// estree the location refers to the entire string including the hash token
|
|
range: node.range,
|
|
loc: node.loc,
|
|
};
|
|
}
|
|
|
|
mapExportNamedDeclaration(nodeUnprocessed: HermesNode): HermesNode {
|
|
const node = super.mapExportNamedDeclaration(nodeUnprocessed);
|
|
|
|
const namespaceSpecifier = node.specifiers.find(
|
|
spec => spec.type === 'ExportNamespaceSpecifier',
|
|
);
|
|
if (namespaceSpecifier != null) {
|
|
if (node.specifiers.length !== 1) {
|
|
// this should already a hermes parser error - but let's be absolutely sure we're aligned with the spec
|
|
throw new Error('Cannot use an export all with any other specifiers');
|
|
}
|
|
return {
|
|
type: 'ExportAllDeclaration',
|
|
source: node.source,
|
|
exportKind: node.exportKind ?? 'value',
|
|
exported: namespaceSpecifier.exported,
|
|
range: node.range,
|
|
loc: node.loc,
|
|
};
|
|
}
|
|
|
|
return node;
|
|
}
|
|
|
|
mapExportAllDeclaration(nodeUnprocessed: HermesNode): HermesNode {
|
|
const node = super.mapExportAllDeclaration(nodeUnprocessed);
|
|
node.exported = node.exported ?? null;
|
|
return node;
|
|
}
|
|
}
|