typescript-estree
Version:
A parser that converts TypeScript source code into an ESTree compatible form
663 lines (662 loc) • 23.4 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
/**
* @fileoverview Utilities for finding and converting ts.Nodes into ESTreeNodes
* @author James Henry <https://github.com/JamesHenry>
* @copyright jQuery Foundation and other contributors, https://jquery.org/
* MIT License
*/
const typescript_1 = __importDefault(require("typescript"));
const lodash_unescape_1 = __importDefault(require("lodash.unescape"));
const ast_node_types_1 = require("./ast-node-types");
const SyntaxKind = typescript_1.default.SyntaxKind;
const ASSIGNMENT_OPERATORS = [
SyntaxKind.EqualsToken,
SyntaxKind.PlusEqualsToken,
SyntaxKind.MinusEqualsToken,
SyntaxKind.AsteriskEqualsToken,
SyntaxKind.AsteriskAsteriskEqualsToken,
SyntaxKind.SlashEqualsToken,
SyntaxKind.PercentEqualsToken,
SyntaxKind.LessThanLessThanEqualsToken,
SyntaxKind.GreaterThanGreaterThanEqualsToken,
SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken,
SyntaxKind.AmpersandEqualsToken,
SyntaxKind.BarEqualsToken,
SyntaxKind.CaretEqualsToken
];
const LOGICAL_OPERATORS = [
SyntaxKind.BarBarToken,
SyntaxKind.AmpersandAmpersandToken
];
const TOKEN_TO_TEXT = {
[SyntaxKind.OpenBraceToken]: '{',
[SyntaxKind.CloseBraceToken]: '}',
[SyntaxKind.OpenParenToken]: '(',
[SyntaxKind.CloseParenToken]: ')',
[SyntaxKind.OpenBracketToken]: '[',
[SyntaxKind.CloseBracketToken]: ']',
[SyntaxKind.DotToken]: '.',
[SyntaxKind.DotDotDotToken]: '...',
[SyntaxKind.SemicolonToken]: ',',
[SyntaxKind.CommaToken]: ',',
[SyntaxKind.LessThanToken]: '<',
[SyntaxKind.GreaterThanToken]: '>',
[SyntaxKind.LessThanEqualsToken]: '<=',
[SyntaxKind.GreaterThanEqualsToken]: '>=',
[SyntaxKind.EqualsEqualsToken]: '==',
[SyntaxKind.ExclamationEqualsToken]: '!=',
[SyntaxKind.EqualsEqualsEqualsToken]: '===',
[SyntaxKind.InstanceOfKeyword]: 'instanceof',
[SyntaxKind.ExclamationEqualsEqualsToken]: '!==',
[SyntaxKind.EqualsGreaterThanToken]: '=>',
[SyntaxKind.PlusToken]: '+',
[SyntaxKind.MinusToken]: '-',
[SyntaxKind.AsteriskToken]: '*',
[SyntaxKind.AsteriskAsteriskToken]: '**',
[SyntaxKind.SlashToken]: '/',
[SyntaxKind.PercentToken]: '%',
[SyntaxKind.PlusPlusToken]: '++',
[SyntaxKind.MinusMinusToken]: '--',
[SyntaxKind.LessThanLessThanToken]: '<<',
[SyntaxKind.LessThanSlashToken]: '</',
[SyntaxKind.GreaterThanGreaterThanToken]: '>>',
[SyntaxKind.GreaterThanGreaterThanGreaterThanToken]: '>>>',
[SyntaxKind.AmpersandToken]: '&',
[SyntaxKind.BarToken]: '|',
[SyntaxKind.CaretToken]: '^',
[SyntaxKind.ExclamationToken]: '!',
[SyntaxKind.TildeToken]: '~',
[SyntaxKind.AmpersandAmpersandToken]: '&&',
[SyntaxKind.BarBarToken]: '||',
[SyntaxKind.QuestionToken]: '?',
[SyntaxKind.ColonToken]: ':',
[SyntaxKind.EqualsToken]: '=',
[SyntaxKind.PlusEqualsToken]: '+=',
[SyntaxKind.MinusEqualsToken]: '-=',
[SyntaxKind.AsteriskEqualsToken]: '*=',
[SyntaxKind.AsteriskAsteriskEqualsToken]: '**=',
[SyntaxKind.SlashEqualsToken]: '/=',
[SyntaxKind.PercentEqualsToken]: '%=',
[SyntaxKind.LessThanLessThanEqualsToken]: '<<=',
[SyntaxKind.GreaterThanGreaterThanEqualsToken]: '>>=',
[SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken]: '>>>=',
[SyntaxKind.AmpersandEqualsToken]: '&=',
[SyntaxKind.BarEqualsToken]: '|=',
[SyntaxKind.CaretEqualsToken]: '^=',
[SyntaxKind.AtToken]: '@',
[SyntaxKind.InKeyword]: 'in',
[SyntaxKind.UniqueKeyword]: 'unique',
[SyntaxKind.KeyOfKeyword]: 'keyof',
[SyntaxKind.NewKeyword]: 'new',
[SyntaxKind.ImportKeyword]: 'import'
};
/**
* Returns true if the given ts.Token is the assignment operator
* @param {ts.Token} operator the operator token
* @returns {boolean} is assignment
*/
function isAssignmentOperator(operator) {
return ASSIGNMENT_OPERATORS.indexOf(operator.kind) > -1;
}
exports.isAssignmentOperator = isAssignmentOperator;
/**
* Returns true if the given ts.Token is a logical operator
* @param {ts.Token} operator the operator token
* @returns {boolean} is a logical operator
*/
function isLogicalOperator(operator) {
return LOGICAL_OPERATORS.indexOf(operator.kind) > -1;
}
exports.isLogicalOperator = isLogicalOperator;
/**
* Returns the string form of the given TSToken SyntaxKind
* @param {number} kind the token's SyntaxKind
* @returns {string} the token applicable token as a string
*/
function getTextForTokenKind(kind) {
return TOKEN_TO_TEXT[kind];
}
exports.getTextForTokenKind = getTextForTokenKind;
/**
* Returns true if the given ts.Node is a valid ESTree class member
* @param {ts.Node} node TypeScript AST node
* @returns {boolean} is valid ESTree class member
*/
function isESTreeClassMember(node) {
return node.kind !== SyntaxKind.SemicolonClassElement;
}
exports.isESTreeClassMember = isESTreeClassMember;
/**
* Checks if a ts.Node has a modifier
* @param {ts.KeywordSyntaxKind} modifierKind TypeScript SyntaxKind modifier
* @param {ts.Node} node TypeScript AST node
* @returns {boolean} has the modifier specified
*/
function hasModifier(modifierKind, node) {
return (!!node.modifiers &&
!!node.modifiers.length &&
node.modifiers.some(modifier => modifier.kind === modifierKind));
}
exports.hasModifier = hasModifier;
/**
* Get last last modifier in ast
* @param node TypeScript AST node
* @returns returns last modifier if present or null
*/
function getLastModifier(node) {
return ((!!node.modifiers &&
!!node.modifiers.length &&
node.modifiers[node.modifiers.length - 1]) ||
null);
}
exports.getLastModifier = getLastModifier;
/**
* Returns true if the given ts.Token is a comma
* @param {ts.Node} token the TypeScript token
* @returns {boolean} is comma
*/
function isComma(token) {
return token.kind === SyntaxKind.CommaToken;
}
exports.isComma = isComma;
/**
* Returns true if the given ts.Node is a comment
* @param {ts.Node} node the TypeScript node
* @returns {boolean} is comment
*/
function isComment(node) {
return (node.kind === SyntaxKind.SingleLineCommentTrivia ||
node.kind === SyntaxKind.MultiLineCommentTrivia);
}
exports.isComment = isComment;
/**
* Returns true if the given ts.Node is a JSDoc comment
* @param {ts.Node} node the TypeScript node
* @returns {boolean} is JSDoc comment
*/
function isJSDocComment(node) {
return node.kind === SyntaxKind.JSDocComment;
}
exports.isJSDocComment = isJSDocComment;
/**
* Returns the binary expression type of the given ts.Token
* @param {ts.Token} operator the operator token
* @returns {string} the binary expression type
*/
function getBinaryExpressionType(operator) {
if (isAssignmentOperator(operator)) {
return ast_node_types_1.AST_NODE_TYPES.AssignmentExpression;
}
else if (isLogicalOperator(operator)) {
return ast_node_types_1.AST_NODE_TYPES.LogicalExpression;
}
return ast_node_types_1.AST_NODE_TYPES.BinaryExpression;
}
exports.getBinaryExpressionType = getBinaryExpressionType;
/**
* Returns line and column data for the given start and end positions,
* for the given AST
* @param {number} start start data
* @param {number} end end data
* @param {ts.SourceFile} ast the AST object
* @returns {ESTreeNodeLoc} the loc data
*/
function getLocFor(start, end, ast) {
const startLoc = ast.getLineAndCharacterOfPosition(start), endLoc = ast.getLineAndCharacterOfPosition(end);
return {
start: {
line: startLoc.line + 1,
column: startLoc.character
},
end: {
line: endLoc.line + 1,
column: endLoc.character
}
};
}
exports.getLocFor = getLocFor;
/**
* Check whatever node can contain directive
* @param {ts.Node} node
* @returns {boolean} returns true if node can contain directive
*/
function canContainDirective(node) {
switch (node.kind) {
case typescript_1.default.SyntaxKind.SourceFile:
case typescript_1.default.SyntaxKind.ModuleBlock:
return true;
case typescript_1.default.SyntaxKind.Block:
switch (node.parent.kind) {
case typescript_1.default.SyntaxKind.Constructor:
case typescript_1.default.SyntaxKind.GetAccessor:
case typescript_1.default.SyntaxKind.SetAccessor:
case typescript_1.default.SyntaxKind.ArrowFunction:
case typescript_1.default.SyntaxKind.FunctionExpression:
case typescript_1.default.SyntaxKind.FunctionDeclaration:
case typescript_1.default.SyntaxKind.MethodDeclaration:
return true;
default:
return false;
}
default:
return false;
}
}
exports.canContainDirective = canContainDirective;
/**
* Returns line and column data for the given ts.Node or ts.Token,
* for the given AST
* @param {ts.Node} nodeOrToken the ts.Node or ts.Token
* @param {ts.SourceFile} ast the AST object
* @returns {ESTreeLoc} the loc data
*/
function getLoc(nodeOrToken, ast) {
return getLocFor(nodeOrToken.getStart(ast), nodeOrToken.end, ast);
}
exports.getLoc = getLoc;
/**
* Returns true if a given ts.Node is a token
* @param {ts.Node} node the ts.Node
* @returns {boolean} is a token
*/
function isToken(node) {
return (node.kind >= SyntaxKind.FirstToken && node.kind <= SyntaxKind.LastToken);
}
exports.isToken = isToken;
/**
* Returns true if a given ts.Node is a JSX token
* @param {ts.Node} node ts.Node to be checked
* @returns {boolean} is a JSX token
*/
function isJSXToken(node) {
return (node.kind >= SyntaxKind.JsxElement && node.kind <= SyntaxKind.JsxAttribute);
}
exports.isJSXToken = isJSXToken;
/**
* Returns the declaration kind of the given ts.Node
* @param {ts.VariableDeclarationList} node TypeScript AST node
* @returns {string} declaration kind
*/
function getDeclarationKind(node) {
if (node.flags & typescript_1.default.NodeFlags.Let) {
return 'let';
}
if (node.flags & typescript_1.default.NodeFlags.Const) {
return 'const';
}
return 'var';
}
exports.getDeclarationKind = getDeclarationKind;
/**
* Gets a ts.Node's accessibility level
* @param {ts.Node} node The ts.Node
* @returns {string | null} accessibility "public", "protected", "private", or null
*/
function getTSNodeAccessibility(node) {
const modifiers = node.modifiers;
if (!modifiers) {
return null;
}
for (let i = 0; i < modifiers.length; i++) {
const modifier = modifiers[i];
switch (modifier.kind) {
case SyntaxKind.PublicKeyword:
return 'public';
case SyntaxKind.ProtectedKeyword:
return 'protected';
case SyntaxKind.PrivateKeyword:
return 'private';
default:
break;
}
}
return null;
}
exports.getTSNodeAccessibility = getTSNodeAccessibility;
/**
* Finds the next token based on the previous one and its parent
* Had to copy this from TS instead of using TS's version because theirs doesn't pass the ast to getChildren
* @param {ts.Node} previousToken The previous TSToken
* @param {ts.Node} parent The parent TSNode
* @param {ts.SourceFile} ast The TS AST
* @returns {ts.Node|undefined} the next TSToken
*/
function findNextToken(previousToken, parent, ast) {
return find(parent);
function find(n) {
if (typescript_1.default.isToken(n) && n.pos === previousToken.end) {
// this is token that starts at the end of previous token - return it
return n;
}
return firstDefined(n.getChildren(ast), (child) => {
const shouldDiveInChildNode =
// previous token is enclosed somewhere in the child
(child.pos <= previousToken.pos && child.end > previousToken.end) ||
// previous token ends exactly at the beginning of child
child.pos === previousToken.end;
return shouldDiveInChildNode && nodeHasTokens(child, ast)
? find(child)
: undefined;
});
}
}
exports.findNextToken = findNextToken;
/**
* Find the first matching token based on the given predicate function.
* @param {ts.Node} previousToken The previous ts.Token
* @param {ts.Node} parent The parent ts.Node
* @param {Function} predicate The predicate function to apply to each checked token
* @param {ts.SourceFile} ast The TS AST
* @returns {ts.Node|undefined} a matching ts.Token
*/
function findFirstMatchingToken(previousToken, parent, predicate, ast) {
while (previousToken) {
if (predicate(previousToken)) {
return previousToken;
}
previousToken = findNextToken(previousToken, parent, ast);
}
return undefined;
}
exports.findFirstMatchingToken = findFirstMatchingToken;
/**
* Find the first matching ancestor based on the given predicate function.
* @param {ts.Node} node The current ts.Node
* @param {Function} predicate The predicate function to apply to each checked ancestor
* @returns {ts.Node|undefined} a matching parent ts.Node
*/
function findFirstMatchingAncestor(node, predicate) {
while (node) {
if (predicate(node)) {
return node;
}
node = node.parent;
}
return undefined;
}
exports.findFirstMatchingAncestor = findFirstMatchingAncestor;
/**
* Returns true if a given ts.Node has a JSX token within its hierarchy
* @param {ts.Node} node ts.Node to be checked
* @returns {boolean} has JSX ancestor
*/
function hasJSXAncestor(node) {
return !!findFirstMatchingAncestor(node, isJSXToken);
}
exports.hasJSXAncestor = hasJSXAncestor;
/**
* Unescape the text content of string literals, e.g. & -> &
* @param {string} text The escaped string literal text.
* @returns {string} The unescaped string literal text.
*/
function unescapeStringLiteralText(text) {
return lodash_unescape_1.default(text);
}
exports.unescapeStringLiteralText = unescapeStringLiteralText;
/**
* Returns true if a given ts.Node is a computed property
* @param {ts.Node} node ts.Node to be checked
* @returns {boolean} is Computed Property
*/
function isComputedProperty(node) {
return node.kind === SyntaxKind.ComputedPropertyName;
}
exports.isComputedProperty = isComputedProperty;
/**
* Returns true if a given ts.Node is optional (has QuestionToken)
* @param {ts.Node} node ts.Node to be checked
* @returns {boolean} is Optional
*/
function isOptional(node) {
return node.questionToken
? node.questionToken.kind === SyntaxKind.QuestionToken
: false;
}
exports.isOptional = isOptional;
/**
* Fixes the exports of the given ts.Node
* @param {ts.Node} node the ts.Node
* @param {ESTreeNode} result result
* @param {ts.SourceFile} ast the AST
* @returns {ESTreeNode} the ESTreeNode with fixed exports
*/
function fixExports(node, result, ast) {
// check for exports
if (node.modifiers && node.modifiers[0].kind === SyntaxKind.ExportKeyword) {
const exportKeyword = node.modifiers[0], nextModifier = node.modifiers[1], lastModifier = node.modifiers[node.modifiers.length - 1], declarationIsDefault = nextModifier && nextModifier.kind === SyntaxKind.DefaultKeyword, varToken = findNextToken(lastModifier, ast, ast);
result.range[0] = varToken.getStart(ast);
result.loc = getLocFor(result.range[0], result.range[1], ast);
const declarationType = declarationIsDefault
? ast_node_types_1.AST_NODE_TYPES.ExportDefaultDeclaration
: ast_node_types_1.AST_NODE_TYPES.ExportNamedDeclaration;
const newResult = {
type: declarationType,
declaration: result,
range: [exportKeyword.getStart(ast), result.range[1]],
loc: getLocFor(exportKeyword.getStart(ast), result.range[1], ast)
};
if (!declarationIsDefault) {
newResult.specifiers = [];
newResult.source = null;
}
return newResult;
}
return result;
}
exports.fixExports = fixExports;
/**
* Returns the type of a given ts.Token
* @param {ts.Token} token the ts.Token
* @returns {string} the token type
*/
function getTokenType(token) {
// Need two checks for keywords since some are also identifiers
if (token.originalKeywordKind) {
switch (token.originalKeywordKind) {
case SyntaxKind.NullKeyword:
return 'Null';
case SyntaxKind.GetKeyword:
case SyntaxKind.SetKeyword:
case SyntaxKind.TypeKeyword:
case SyntaxKind.ModuleKeyword:
return 'Identifier';
default:
return 'Keyword';
}
}
if (token.kind >= SyntaxKind.FirstKeyword &&
token.kind <= SyntaxKind.LastFutureReservedWord) {
if (token.kind === SyntaxKind.FalseKeyword ||
token.kind === SyntaxKind.TrueKeyword) {
return 'Boolean';
}
return 'Keyword';
}
if (token.kind >= SyntaxKind.FirstPunctuation &&
token.kind <= SyntaxKind.LastBinaryOperator) {
return 'Punctuator';
}
if (token.kind >= SyntaxKind.NoSubstitutionTemplateLiteral &&
token.kind <= SyntaxKind.TemplateTail) {
return 'Template';
}
switch (token.kind) {
case SyntaxKind.NumericLiteral:
return 'Numeric';
case SyntaxKind.JsxText:
return 'JSXText';
case SyntaxKind.StringLiteral:
// A TypeScript-StringLiteral token with a TypeScript-JsxAttribute or TypeScript-JsxElement parent,
// must actually be an ESTree-JSXText token
if (token.parent &&
(token.parent.kind === SyntaxKind.JsxAttribute ||
token.parent.kind === SyntaxKind.JsxElement)) {
return 'JSXText';
}
return 'String';
case SyntaxKind.RegularExpressionLiteral:
return 'RegularExpression';
case SyntaxKind.Identifier:
case SyntaxKind.ConstructorKeyword:
case SyntaxKind.GetKeyword:
case SyntaxKind.SetKeyword:
// falls through
default:
}
// Some JSX tokens have to be determined based on their parent
if (token.parent) {
if (token.kind === SyntaxKind.Identifier &&
token.parent.kind === SyntaxKind.PropertyAccessExpression &&
hasJSXAncestor(token)) {
return 'JSXIdentifier';
}
if (isJSXToken(token.parent)) {
if (token.kind === SyntaxKind.PropertyAccessExpression) {
return 'JSXMemberExpression';
}
if (token.kind === SyntaxKind.Identifier) {
return 'JSXIdentifier';
}
}
}
return 'Identifier';
}
exports.getTokenType = getTokenType;
/**
* Extends and formats a given ts.Token, for a given AST
* @param {ts.Node} token the ts.Token
* @param {ts.SourceFile} ast the AST object
* @returns {ESTreeToken} the converted ESTreeToken
*/
function convertToken(token, ast) {
const start = token.kind === SyntaxKind.JsxText
? token.getFullStart()
: token.getStart(ast), end = token.getEnd(), value = ast.text.slice(start, end), newToken = {
type: getTokenType(token),
value,
range: [start, end],
loc: getLocFor(start, end, ast)
};
if (newToken.type === 'RegularExpression') {
newToken.regex = {
pattern: value.slice(1, value.lastIndexOf('/')),
flags: value.slice(value.lastIndexOf('/') + 1)
};
}
return newToken;
}
exports.convertToken = convertToken;
/**
* Converts all tokens for the given AST
* @param {ts.SourceFile} ast the AST object
* @returns {ESTreeToken[]} the converted ESTreeTokens
*/
function convertTokens(ast) {
const result = [];
/**
* @param {ts.Node} node the ts.Node
* @returns {void}
*/
function walk(node) {
// TypeScript generates tokens for types in JSDoc blocks. Comment tokens
// and their children should not be walked or added to the resulting tokens list.
if (isComment(node) || isJSDocComment(node)) {
return;
}
if (isToken(node) && node.kind !== SyntaxKind.EndOfFileToken) {
const converted = convertToken(node, ast);
if (converted) {
result.push(converted);
}
}
else {
node.getChildren(ast).forEach(walk);
}
}
walk(ast);
return result;
}
exports.convertTokens = convertTokens;
/**
* Get container token node between range
* @param {ts.SourceFile} ast the AST object
* @param {number} start The index at which the comment starts.
* @param {number} end The index at which the comment ends.
* @returns {ts.Node} typescript container token
* @private
*/
function getNodeContainer(ast, start, end) {
let container = null;
/**
* @param {ts.Node} node the ts.Node
* @returns {void}
*/
function walk(node) {
const nodeStart = node.pos;
const nodeEnd = node.end;
if (start >= nodeStart && end <= nodeEnd) {
if (isToken(node)) {
container = node;
}
else {
node.getChildren().forEach(walk);
}
}
}
walk(ast);
return container;
}
exports.getNodeContainer = getNodeContainer;
/**
* @param {ts.SourceFile} ast the AST object
* @param {number} start the index at which the error starts
* @param {string} message the error message
* @returns {Object} converted error object
*/
function createError(ast, start, message) {
const loc = ast.getLineAndCharacterOfPosition(start);
return {
index: start,
lineNumber: loc.line + 1,
column: loc.character,
message
};
}
exports.createError = createError;
/**
* @param {ts.Node} n the TSNode
* @param {ts.SourceFile} ast the TS AST
*/
function nodeHasTokens(n, ast) {
// If we have a token or node that has a non-zero width, it must have tokens.
// Note: getWidth() does not take trivia into account.
return n.kind === SyntaxKind.EndOfFileToken
? !!n.jsDoc
: n.getWidth(ast) !== 0;
}
exports.nodeHasTokens = nodeHasTokens;
/**
* Like `forEach`, but suitable for use with numbers and strings (which may be falsy).
* @template T
* @template U
* @param {ReadonlyArray<T>|undefined} array
* @param {(element: T, index: number) => (U|undefined)} callback
* @returns {U|undefined}
*/
function firstDefined(array, callback) {
if (array === undefined) {
return undefined;
}
for (let i = 0; i < array.length; i++) {
const result = callback(array[i], i);
if (result !== undefined) {
return result;
}
}
return undefined;
}
exports.firstDefined = firstDefined;