353 lines
11 KiB
JavaScript
353 lines
11 KiB
JavaScript
|
|
|
|
import {ContextualKeyword} from "../parser/tokenizer/keywords";
|
|
import {TokenType as tt} from "../parser/tokenizer/types";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
* Get information about the class fields for this class, given a token processor pointing to the
|
|
* open-brace at the start of the class.
|
|
*/
|
|
export default function getClassInfo(
|
|
rootTransformer,
|
|
tokens,
|
|
nameManager,
|
|
disableESTransforms,
|
|
) {
|
|
const snapshot = tokens.snapshot();
|
|
|
|
const headerInfo = processClassHeader(tokens);
|
|
|
|
let constructorInitializerStatements = [];
|
|
const instanceInitializerNames = [];
|
|
const staticInitializerNames = [];
|
|
let constructorInsertPos = null;
|
|
const fields = [];
|
|
const rangesToRemove = [];
|
|
|
|
const classContextId = tokens.currentToken().contextId;
|
|
if (classContextId == null) {
|
|
throw new Error("Expected non-null class context ID on class open-brace.");
|
|
}
|
|
|
|
tokens.nextToken();
|
|
while (!tokens.matchesContextIdAndLabel(tt.braceR, classContextId)) {
|
|
if (tokens.matchesContextual(ContextualKeyword._constructor) && !tokens.currentToken().isType) {
|
|
({constructorInitializerStatements, constructorInsertPos} = processConstructor(tokens));
|
|
} else if (tokens.matches1(tt.semi)) {
|
|
if (!disableESTransforms) {
|
|
rangesToRemove.push({start: tokens.currentIndex(), end: tokens.currentIndex() + 1});
|
|
}
|
|
tokens.nextToken();
|
|
} else if (tokens.currentToken().isType) {
|
|
tokens.nextToken();
|
|
} else {
|
|
// Either a method or a field. Skip to the identifier part.
|
|
const statementStartIndex = tokens.currentIndex();
|
|
let isStatic = false;
|
|
let isESPrivate = false;
|
|
let isDeclareOrAbstract = false;
|
|
while (isAccessModifier(tokens.currentToken())) {
|
|
if (tokens.matches1(tt._static)) {
|
|
isStatic = true;
|
|
}
|
|
if (tokens.matches1(tt.hash)) {
|
|
isESPrivate = true;
|
|
}
|
|
if (tokens.matches1(tt._declare) || tokens.matches1(tt._abstract)) {
|
|
isDeclareOrAbstract = true;
|
|
}
|
|
tokens.nextToken();
|
|
}
|
|
if (isStatic && tokens.matches1(tt.braceL)) {
|
|
// This is a static block, so don't process it in any special way.
|
|
skipToNextClassElement(tokens, classContextId);
|
|
continue;
|
|
}
|
|
if (isESPrivate) {
|
|
// Sucrase doesn't attempt to transpile private fields; just leave them as-is.
|
|
skipToNextClassElement(tokens, classContextId);
|
|
continue;
|
|
}
|
|
if (
|
|
tokens.matchesContextual(ContextualKeyword._constructor) &&
|
|
!tokens.currentToken().isType
|
|
) {
|
|
({constructorInitializerStatements, constructorInsertPos} = processConstructor(tokens));
|
|
continue;
|
|
}
|
|
|
|
const nameStartIndex = tokens.currentIndex();
|
|
skipFieldName(tokens);
|
|
if (tokens.matches1(tt.lessThan) || tokens.matches1(tt.parenL)) {
|
|
// This is a method, so nothing to process.
|
|
skipToNextClassElement(tokens, classContextId);
|
|
continue;
|
|
}
|
|
// There might be a type annotation that we need to skip.
|
|
while (tokens.currentToken().isType) {
|
|
tokens.nextToken();
|
|
}
|
|
if (tokens.matches1(tt.eq)) {
|
|
const equalsIndex = tokens.currentIndex();
|
|
// This is an initializer, so we need to wrap in an initializer method.
|
|
const valueEnd = tokens.currentToken().rhsEndIndex;
|
|
if (valueEnd == null) {
|
|
throw new Error("Expected rhsEndIndex on class field assignment.");
|
|
}
|
|
tokens.nextToken();
|
|
while (tokens.currentIndex() < valueEnd) {
|
|
rootTransformer.processToken();
|
|
}
|
|
let initializerName;
|
|
if (isStatic) {
|
|
initializerName = nameManager.claimFreeName("__initStatic");
|
|
staticInitializerNames.push(initializerName);
|
|
} else {
|
|
initializerName = nameManager.claimFreeName("__init");
|
|
instanceInitializerNames.push(initializerName);
|
|
}
|
|
// Fields start at the name, so `static x = 1;` has a field range of `x = 1;`.
|
|
fields.push({
|
|
initializerName,
|
|
equalsIndex,
|
|
start: nameStartIndex,
|
|
end: tokens.currentIndex(),
|
|
});
|
|
} else if (!disableESTransforms || isDeclareOrAbstract) {
|
|
// This is a regular field declaration, like `x;`. With the class transform enabled, we just
|
|
// remove the line so that no output is produced. With the class transform disabled, we
|
|
// usually want to preserve the declaration (but still strip types), but if the `declare`
|
|
// or `abstract` keyword is specified, we should remove the line to avoid initializing the
|
|
// value to undefined.
|
|
rangesToRemove.push({start: statementStartIndex, end: tokens.currentIndex()});
|
|
}
|
|
}
|
|
}
|
|
|
|
tokens.restoreToSnapshot(snapshot);
|
|
if (disableESTransforms) {
|
|
// With ES transforms disabled, we don't want to transform regular class
|
|
// field declarations, and we don't need to do any additional tricks to
|
|
// reference the constructor for static init, but we still need to transform
|
|
// TypeScript field initializers defined as constructor parameters and we
|
|
// still need to remove `declare` fields. For now, we run the same code
|
|
// path but omit any field information, as if the class had no field
|
|
// declarations. In the future, when we fully drop the class fields
|
|
// transform, we can simplify this code significantly.
|
|
return {
|
|
headerInfo,
|
|
constructorInitializerStatements,
|
|
instanceInitializerNames: [],
|
|
staticInitializerNames: [],
|
|
constructorInsertPos,
|
|
fields: [],
|
|
rangesToRemove,
|
|
};
|
|
} else {
|
|
return {
|
|
headerInfo,
|
|
constructorInitializerStatements,
|
|
instanceInitializerNames,
|
|
staticInitializerNames,
|
|
constructorInsertPos,
|
|
fields,
|
|
rangesToRemove,
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Move the token processor to the next method/field in the class.
|
|
*
|
|
* To do that, we seek forward to the next start of a class name (either an open
|
|
* bracket or an identifier, or the closing curly brace), then seek backward to
|
|
* include any access modifiers.
|
|
*/
|
|
function skipToNextClassElement(tokens, classContextId) {
|
|
tokens.nextToken();
|
|
while (tokens.currentToken().contextId !== classContextId) {
|
|
tokens.nextToken();
|
|
}
|
|
while (isAccessModifier(tokens.tokenAtRelativeIndex(-1))) {
|
|
tokens.previousToken();
|
|
}
|
|
}
|
|
|
|
function processClassHeader(tokens) {
|
|
const classToken = tokens.currentToken();
|
|
const contextId = classToken.contextId;
|
|
if (contextId == null) {
|
|
throw new Error("Expected context ID on class token.");
|
|
}
|
|
const isExpression = classToken.isExpression;
|
|
if (isExpression == null) {
|
|
throw new Error("Expected isExpression on class token.");
|
|
}
|
|
let className = null;
|
|
let hasSuperclass = false;
|
|
tokens.nextToken();
|
|
if (tokens.matches1(tt.name)) {
|
|
className = tokens.identifierName();
|
|
}
|
|
while (!tokens.matchesContextIdAndLabel(tt.braceL, contextId)) {
|
|
// If this has a superclass, there will always be an `extends` token. If it doesn't have a
|
|
// superclass, only type parameters and `implements` clauses can show up here, all of which
|
|
// consist only of type tokens. A declaration like `class A<B extends C> {` should *not* count
|
|
// as having a superclass.
|
|
if (tokens.matches1(tt._extends) && !tokens.currentToken().isType) {
|
|
hasSuperclass = true;
|
|
}
|
|
tokens.nextToken();
|
|
}
|
|
return {isExpression, className, hasSuperclass};
|
|
}
|
|
|
|
/**
|
|
* Extract useful information out of a constructor, starting at the "constructor" name.
|
|
*/
|
|
function processConstructor(tokens)
|
|
|
|
|
|
{
|
|
const constructorInitializerStatements = [];
|
|
|
|
tokens.nextToken();
|
|
const constructorContextId = tokens.currentToken().contextId;
|
|
if (constructorContextId == null) {
|
|
throw new Error("Expected context ID on open-paren starting constructor params.");
|
|
}
|
|
// Advance through parameters looking for access modifiers.
|
|
while (!tokens.matchesContextIdAndLabel(tt.parenR, constructorContextId)) {
|
|
if (tokens.currentToken().contextId === constructorContextId) {
|
|
// Current token is an open paren or comma just before a param, so check
|
|
// that param for access modifiers.
|
|
tokens.nextToken();
|
|
if (isAccessModifier(tokens.currentToken())) {
|
|
tokens.nextToken();
|
|
while (isAccessModifier(tokens.currentToken())) {
|
|
tokens.nextToken();
|
|
}
|
|
const token = tokens.currentToken();
|
|
if (token.type !== tt.name) {
|
|
throw new Error("Expected identifier after access modifiers in constructor arg.");
|
|
}
|
|
const name = tokens.identifierNameForToken(token);
|
|
constructorInitializerStatements.push(`this.${name} = ${name}`);
|
|
}
|
|
} else {
|
|
tokens.nextToken();
|
|
}
|
|
}
|
|
// )
|
|
tokens.nextToken();
|
|
// Constructor type annotations are invalid, but skip them anyway since
|
|
// they're easy to skip.
|
|
while (tokens.currentToken().isType) {
|
|
tokens.nextToken();
|
|
}
|
|
let constructorInsertPos = tokens.currentIndex();
|
|
|
|
// Advance through body looking for a super call.
|
|
let foundSuperCall = false;
|
|
while (!tokens.matchesContextIdAndLabel(tt.braceR, constructorContextId)) {
|
|
if (!foundSuperCall && tokens.matches2(tt._super, tt.parenL)) {
|
|
tokens.nextToken();
|
|
const superCallContextId = tokens.currentToken().contextId;
|
|
if (superCallContextId == null) {
|
|
throw new Error("Expected a context ID on the super call");
|
|
}
|
|
while (!tokens.matchesContextIdAndLabel(tt.parenR, superCallContextId)) {
|
|
tokens.nextToken();
|
|
}
|
|
constructorInsertPos = tokens.currentIndex();
|
|
foundSuperCall = true;
|
|
}
|
|
tokens.nextToken();
|
|
}
|
|
// }
|
|
tokens.nextToken();
|
|
|
|
return {constructorInitializerStatements, constructorInsertPos};
|
|
}
|
|
|
|
/**
|
|
* Determine if this is any token that can go before the name in a method/field.
|
|
*/
|
|
function isAccessModifier(token) {
|
|
return [
|
|
tt._async,
|
|
tt._get,
|
|
tt._set,
|
|
tt.plus,
|
|
tt.minus,
|
|
tt._readonly,
|
|
tt._static,
|
|
tt._public,
|
|
tt._private,
|
|
tt._protected,
|
|
tt._override,
|
|
tt._abstract,
|
|
tt.star,
|
|
tt._declare,
|
|
tt.hash,
|
|
].includes(token.type);
|
|
}
|
|
|
|
/**
|
|
* The next token or set of tokens is either an identifier or an expression in square brackets, for
|
|
* a method or field name.
|
|
*/
|
|
function skipFieldName(tokens) {
|
|
if (tokens.matches1(tt.bracketL)) {
|
|
const startToken = tokens.currentToken();
|
|
const classContextId = startToken.contextId;
|
|
if (classContextId == null) {
|
|
throw new Error("Expected class context ID on computed name open bracket.");
|
|
}
|
|
while (!tokens.matchesContextIdAndLabel(tt.bracketR, classContextId)) {
|
|
tokens.nextToken();
|
|
}
|
|
tokens.nextToken();
|
|
} else {
|
|
tokens.nextToken();
|
|
}
|
|
}
|