TypeScript/src/services/utilities.ts

2759 lines
127 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* @internal */ // Don't expose that we use this
// Based on lib.es6.d.ts
interface PromiseConstructor {
new <T>(executor: (resolve: (value?: T | PromiseLike<T>) => void, reject: (reason?: any) => void) => void): Promise<T>;
reject(reason: any): Promise<never>;
all<T>(values: (T | PromiseLike<T>)[]): Promise<T[]>;
}
/* @internal */
declare var Promise: PromiseConstructor; // eslint-disable-line no-var
/* @internal */
namespace ts {
// These utilities are common to multiple language service features.
//#region
export const scanner: Scanner = createScanner(ScriptTarget.Latest, /*skipTrivia*/ true);
export const enum SemanticMeaning {
None = 0x0,
Value = 0x1,
Type = 0x2,
Namespace = 0x4,
All = Value | Type | Namespace
}
export function getMeaningFromDeclaration(node: Node): SemanticMeaning {
switch (node.kind) {
case SyntaxKind.VariableDeclaration:
return isInJSFile(node) && getJSDocEnumTag(node) ? SemanticMeaning.All : SemanticMeaning.Value;
case SyntaxKind.Parameter:
case SyntaxKind.BindingElement:
case SyntaxKind.PropertyDeclaration:
case SyntaxKind.PropertySignature:
case SyntaxKind.PropertyAssignment:
case SyntaxKind.ShorthandPropertyAssignment:
case SyntaxKind.MethodDeclaration:
case SyntaxKind.MethodSignature:
case SyntaxKind.Constructor:
case SyntaxKind.GetAccessor:
case SyntaxKind.SetAccessor:
case SyntaxKind.FunctionDeclaration:
case SyntaxKind.FunctionExpression:
case SyntaxKind.ArrowFunction:
case SyntaxKind.CatchClause:
case SyntaxKind.JsxAttribute:
return SemanticMeaning.Value;
case SyntaxKind.TypeParameter:
case SyntaxKind.InterfaceDeclaration:
case SyntaxKind.TypeAliasDeclaration:
case SyntaxKind.TypeLiteral:
return SemanticMeaning.Type;
case SyntaxKind.JSDocTypedefTag:
// If it has no name node, it shares the name with the value declaration below it.
return (node as JSDocTypedefTag).name === undefined ? SemanticMeaning.Value | SemanticMeaning.Type : SemanticMeaning.Type;
case SyntaxKind.EnumMember:
case SyntaxKind.ClassDeclaration:
return SemanticMeaning.Value | SemanticMeaning.Type;
case SyntaxKind.ModuleDeclaration:
if (isAmbientModule(<ModuleDeclaration>node)) {
return SemanticMeaning.Namespace | SemanticMeaning.Value;
}
else if (getModuleInstanceState(node as ModuleDeclaration) === ModuleInstanceState.Instantiated) {
return SemanticMeaning.Namespace | SemanticMeaning.Value;
}
else {
return SemanticMeaning.Namespace;
}
case SyntaxKind.EnumDeclaration:
case SyntaxKind.NamedImports:
case SyntaxKind.ImportSpecifier:
case SyntaxKind.ImportEqualsDeclaration:
case SyntaxKind.ImportDeclaration:
case SyntaxKind.ExportAssignment:
case SyntaxKind.ExportDeclaration:
return SemanticMeaning.All;
// An external module can be a Value
case SyntaxKind.SourceFile:
return SemanticMeaning.Namespace | SemanticMeaning.Value;
}
return SemanticMeaning.All;
}
export function getMeaningFromLocation(node: Node): SemanticMeaning {
node = getAdjustedReferenceLocation(node);
if (node.kind === SyntaxKind.SourceFile) {
return SemanticMeaning.Value;
}
else if (node.parent.kind === SyntaxKind.ExportAssignment || node.parent.kind === SyntaxKind.ExternalModuleReference) {
return SemanticMeaning.All;
}
else if (isInRightSideOfInternalImportEqualsDeclaration(node)) {
return getMeaningFromRightHandSideOfImportEquals(node as Identifier);
}
else if (isDeclarationName(node)) {
return getMeaningFromDeclaration(node.parent);
}
else if (isTypeReference(node)) {
return SemanticMeaning.Type;
}
else if (isNamespaceReference(node)) {
return SemanticMeaning.Namespace;
}
else if (isTypeParameterDeclaration(node.parent)) {
Debug.assert(isJSDocTemplateTag(node.parent.parent)); // Else would be handled by isDeclarationName
return SemanticMeaning.Type;
}
else if (isLiteralTypeNode(node.parent)) {
// This might be T["name"], which is actually referencing a property and not a type. So allow both meanings.
return SemanticMeaning.Type | SemanticMeaning.Value;
}
else {
return SemanticMeaning.Value;
}
}
function getMeaningFromRightHandSideOfImportEquals(node: Node): SemanticMeaning {
// import a = |b|; // Namespace
// import a = |b.c|; // Value, type, namespace
// import a = |b.c|.d; // Namespace
const name = node.kind === SyntaxKind.QualifiedName ? node : isQualifiedName(node.parent) && node.parent.right === node ? node.parent : undefined;
return name && name.parent.kind === SyntaxKind.ImportEqualsDeclaration ? SemanticMeaning.All : SemanticMeaning.Namespace;
}
export function isInRightSideOfInternalImportEqualsDeclaration(node: Node) {
while (node.parent.kind === SyntaxKind.QualifiedName) {
node = node.parent;
}
return isInternalModuleImportEqualsDeclaration(node.parent) && node.parent.moduleReference === node;
}
function isNamespaceReference(node: Node): boolean {
return isQualifiedNameNamespaceReference(node) || isPropertyAccessNamespaceReference(node);
}
function isQualifiedNameNamespaceReference(node: Node): boolean {
let root = node;
let isLastClause = true;
if (root.parent.kind === SyntaxKind.QualifiedName) {
while (root.parent && root.parent.kind === SyntaxKind.QualifiedName) {
root = root.parent;
}
isLastClause = (<QualifiedName>root).right === node;
}
return root.parent.kind === SyntaxKind.TypeReference && !isLastClause;
}
function isPropertyAccessNamespaceReference(node: Node): boolean {
let root = node;
let isLastClause = true;
if (root.parent.kind === SyntaxKind.PropertyAccessExpression) {
while (root.parent && root.parent.kind === SyntaxKind.PropertyAccessExpression) {
root = root.parent;
}
isLastClause = (<PropertyAccessExpression>root).name === node;
}
if (!isLastClause && root.parent.kind === SyntaxKind.ExpressionWithTypeArguments && root.parent.parent.kind === SyntaxKind.HeritageClause) {
const decl = root.parent.parent.parent;
return (decl.kind === SyntaxKind.ClassDeclaration && (<HeritageClause>root.parent.parent).token === SyntaxKind.ImplementsKeyword) ||
(decl.kind === SyntaxKind.InterfaceDeclaration && (<HeritageClause>root.parent.parent).token === SyntaxKind.ExtendsKeyword);
}
return false;
}
function isTypeReference(node: Node): boolean {
if (isRightSideOfQualifiedNameOrPropertyAccess(node)) {
node = node.parent;
}
switch (node.kind) {
case SyntaxKind.ThisKeyword:
return !isExpressionNode(node);
case SyntaxKind.ThisType:
return true;
}
switch (node.parent.kind) {
case SyntaxKind.TypeReference:
return true;
case SyntaxKind.ImportType:
return !(node.parent as ImportTypeNode).isTypeOf;
case SyntaxKind.ExpressionWithTypeArguments:
return !isExpressionWithTypeArgumentsInClassExtendsClause(<ExpressionWithTypeArguments>node.parent);
}
return false;
}
export function isCallExpressionTarget(node: Node, includeElementAccess = false, skipPastOuterExpressions = false): boolean {
return isCalleeWorker(node, isCallExpression, selectExpressionOfCallOrNewExpressionOrDecorator, includeElementAccess, skipPastOuterExpressions);
}
export function isNewExpressionTarget(node: Node, includeElementAccess = false, skipPastOuterExpressions = false): boolean {
return isCalleeWorker(node, isNewExpression, selectExpressionOfCallOrNewExpressionOrDecorator, includeElementAccess, skipPastOuterExpressions);
}
export function isCallOrNewExpressionTarget(node: Node, includeElementAccess = false, skipPastOuterExpressions = false): boolean {
return isCalleeWorker(node, isCallOrNewExpression, selectExpressionOfCallOrNewExpressionOrDecorator, includeElementAccess, skipPastOuterExpressions);
}
export function isTaggedTemplateTag(node: Node, includeElementAccess = false, skipPastOuterExpressions = false): boolean {
return isCalleeWorker(node, isTaggedTemplateExpression, selectTagOfTaggedTemplateExpression, includeElementAccess, skipPastOuterExpressions);
}
export function isDecoratorTarget(node: Node, includeElementAccess = false, skipPastOuterExpressions = false): boolean {
return isCalleeWorker(node, isDecorator, selectExpressionOfCallOrNewExpressionOrDecorator, includeElementAccess, skipPastOuterExpressions);
}
export function isJsxOpeningLikeElementTagName(node: Node, includeElementAccess = false, skipPastOuterExpressions = false): boolean {
return isCalleeWorker(node, isJsxOpeningLikeElement, selectTagNameOfJsxOpeningLikeElement, includeElementAccess, skipPastOuterExpressions);
}
function selectExpressionOfCallOrNewExpressionOrDecorator(node: CallExpression | NewExpression | Decorator) {
return node.expression;
}
function selectTagOfTaggedTemplateExpression(node: TaggedTemplateExpression) {
return node.tag;
}
function selectTagNameOfJsxOpeningLikeElement(node: JsxOpeningLikeElement) {
return node.tagName;
}
function isCalleeWorker<T extends CallExpression | NewExpression | TaggedTemplateExpression | Decorator | JsxOpeningLikeElement>(node: Node, pred: (node: Node) => node is T, calleeSelector: (node: T) => Expression, includeElementAccess: boolean, skipPastOuterExpressions: boolean) {
let target = includeElementAccess ? climbPastPropertyOrElementAccess(node) : climbPastPropertyAccess(node);
if (skipPastOuterExpressions) {
target = skipOuterExpressions(target);
}
return !!target && !!target.parent && pred(target.parent) && calleeSelector(target.parent) === target;
}
export function climbPastPropertyAccess(node: Node) {
return isRightSideOfPropertyAccess(node) ? node.parent : node;
}
export function climbPastPropertyOrElementAccess(node: Node) {
return isRightSideOfPropertyAccess(node) || isArgumentExpressionOfElementAccess(node) ? node.parent : node;
}
export function getTargetLabel(referenceNode: Node, labelName: string): Identifier | undefined {
while (referenceNode) {
if (referenceNode.kind === SyntaxKind.LabeledStatement && (<LabeledStatement>referenceNode).label.escapedText === labelName) {
return (<LabeledStatement>referenceNode).label;
}
referenceNode = referenceNode.parent;
}
return undefined;
}
export function hasPropertyAccessExpressionWithName(node: CallExpression, funcName: string): boolean {
if (!isPropertyAccessExpression(node.expression)) {
return false;
}
return node.expression.name.text === funcName;
}
export function isJumpStatementTarget(node: Node): node is Identifier & { parent: BreakOrContinueStatement } {
return isIdentifier(node) && tryCast(node.parent, isBreakOrContinueStatement)?.label === node;
}
export function isLabelOfLabeledStatement(node: Node): node is Identifier {
return isIdentifier(node) && tryCast(node.parent, isLabeledStatement)?.label === node;
}
export function isLabelName(node: Node): boolean {
return isLabelOfLabeledStatement(node) || isJumpStatementTarget(node);
}
export function isTagName(node: Node): boolean {
return tryCast(node.parent, isJSDocTag)?.tagName === node;
}
export function isRightSideOfQualifiedName(node: Node) {
return tryCast(node.parent, isQualifiedName)?.right === node;
}
export function isRightSideOfPropertyAccess(node: Node) {
return tryCast(node.parent, isPropertyAccessExpression)?.name === node;
}
export function isArgumentExpressionOfElementAccess(node: Node) {
return tryCast(node.parent, isElementAccessExpression)?.argumentExpression === node;
}
export function isNameOfModuleDeclaration(node: Node) {
return tryCast(node.parent, isModuleDeclaration)?.name === node;
}
export function isNameOfFunctionDeclaration(node: Node): boolean {
return isIdentifier(node) && tryCast(node.parent, isFunctionLike)?.name === node;
}
export function isLiteralNameOfPropertyDeclarationOrIndexAccess(node: StringLiteral | NumericLiteral | NoSubstitutionTemplateLiteral): boolean {
switch (node.parent.kind) {
case SyntaxKind.PropertyDeclaration:
case SyntaxKind.PropertySignature:
case SyntaxKind.PropertyAssignment:
case SyntaxKind.EnumMember:
case SyntaxKind.MethodDeclaration:
case SyntaxKind.MethodSignature:
case SyntaxKind.GetAccessor:
case SyntaxKind.SetAccessor:
case SyntaxKind.ModuleDeclaration:
return getNameOfDeclaration(<Declaration>node.parent) === node;
case SyntaxKind.ElementAccessExpression:
return (<ElementAccessExpression>node.parent).argumentExpression === node;
case SyntaxKind.ComputedPropertyName:
return true;
case SyntaxKind.LiteralType:
return node.parent.parent.kind === SyntaxKind.IndexedAccessType;
default:
return false;
}
}
export function isExpressionOfExternalModuleImportEqualsDeclaration(node: Node) {
return isExternalModuleImportEqualsDeclaration(node.parent.parent) &&
getExternalModuleImportEqualsDeclarationExpression(node.parent.parent) === node;
}
export function getContainerNode(node: Node): Declaration | undefined {
if (isJSDocTypeAlias(node)) {
// This doesn't just apply to the node immediately under the comment, but to everything in its parent's scope.
// node.parent = the JSDoc comment, node.parent.parent = the node having the comment.
// Then we get parent again in the loop.
node = node.parent.parent;
}
while (true) {
node = node.parent;
if (!node) {
return undefined;
}
switch (node.kind) {
case SyntaxKind.SourceFile:
case SyntaxKind.MethodDeclaration:
case SyntaxKind.MethodSignature:
case SyntaxKind.FunctionDeclaration:
case SyntaxKind.FunctionExpression:
case SyntaxKind.GetAccessor:
case SyntaxKind.SetAccessor:
case SyntaxKind.ClassDeclaration:
case SyntaxKind.InterfaceDeclaration:
case SyntaxKind.EnumDeclaration:
case SyntaxKind.ModuleDeclaration:
return <Declaration>node;
}
}
}
export function getNodeKind(node: Node): ScriptElementKind {
switch (node.kind) {
case SyntaxKind.SourceFile:
return isExternalModule(<SourceFile>node) ? ScriptElementKind.moduleElement : ScriptElementKind.scriptElement;
case SyntaxKind.ModuleDeclaration:
return ScriptElementKind.moduleElement;
case SyntaxKind.ClassDeclaration:
case SyntaxKind.ClassExpression:
return ScriptElementKind.classElement;
case SyntaxKind.InterfaceDeclaration: return ScriptElementKind.interfaceElement;
case SyntaxKind.TypeAliasDeclaration:
case SyntaxKind.JSDocCallbackTag:
case SyntaxKind.JSDocTypedefTag:
return ScriptElementKind.typeElement;
case SyntaxKind.EnumDeclaration: return ScriptElementKind.enumElement;
case SyntaxKind.VariableDeclaration:
return getKindOfVariableDeclaration(<VariableDeclaration>node);
case SyntaxKind.BindingElement:
return getKindOfVariableDeclaration(<VariableDeclaration>getRootDeclaration(node));
case SyntaxKind.ArrowFunction:
case SyntaxKind.FunctionDeclaration:
case SyntaxKind.FunctionExpression:
return ScriptElementKind.functionElement;
case SyntaxKind.GetAccessor: return ScriptElementKind.memberGetAccessorElement;
case SyntaxKind.SetAccessor: return ScriptElementKind.memberSetAccessorElement;
case SyntaxKind.MethodDeclaration:
case SyntaxKind.MethodSignature:
return ScriptElementKind.memberFunctionElement;
case SyntaxKind.PropertyAssignment:
const {initializer} = node as PropertyAssignment;
return isFunctionLike(initializer) ? ScriptElementKind.memberFunctionElement : ScriptElementKind.memberVariableElement;
case SyntaxKind.PropertyDeclaration:
case SyntaxKind.PropertySignature:
case SyntaxKind.ShorthandPropertyAssignment:
case SyntaxKind.SpreadAssignment:
return ScriptElementKind.memberVariableElement;
case SyntaxKind.IndexSignature: return ScriptElementKind.indexSignatureElement;
case SyntaxKind.ConstructSignature: return ScriptElementKind.constructSignatureElement;
case SyntaxKind.CallSignature: return ScriptElementKind.callSignatureElement;
case SyntaxKind.Constructor: return ScriptElementKind.constructorImplementationElement;
case SyntaxKind.TypeParameter: return ScriptElementKind.typeParameterElement;
case SyntaxKind.EnumMember: return ScriptElementKind.enumMemberElement;
case SyntaxKind.Parameter: return hasModifier(node, ModifierFlags.ParameterPropertyModifier) ? ScriptElementKind.memberVariableElement : ScriptElementKind.parameterElement;
case SyntaxKind.ImportEqualsDeclaration:
case SyntaxKind.ImportSpecifier:
case SyntaxKind.ExportSpecifier:
case SyntaxKind.NamespaceImport:
case SyntaxKind.NamespaceExport:
return ScriptElementKind.alias;
case SyntaxKind.BinaryExpression:
const kind = getAssignmentDeclarationKind(node as BinaryExpression);
const { right } = node as BinaryExpression;
switch (kind) {
case AssignmentDeclarationKind.ObjectDefinePropertyValue:
case AssignmentDeclarationKind.ObjectDefinePropertyExports:
case AssignmentDeclarationKind.ObjectDefinePrototypeProperty:
case AssignmentDeclarationKind.None:
return ScriptElementKind.unknown;
case AssignmentDeclarationKind.ExportsProperty:
case AssignmentDeclarationKind.ModuleExports:
const rightKind = getNodeKind(right);
return rightKind === ScriptElementKind.unknown ? ScriptElementKind.constElement : rightKind;
case AssignmentDeclarationKind.PrototypeProperty:
return isFunctionExpression(right) ? ScriptElementKind.memberFunctionElement : ScriptElementKind.memberVariableElement;
case AssignmentDeclarationKind.ThisProperty:
return ScriptElementKind.memberVariableElement; // property
case AssignmentDeclarationKind.Property:
// static method / property
return isFunctionExpression(right) ? ScriptElementKind.memberFunctionElement : ScriptElementKind.memberVariableElement;
case AssignmentDeclarationKind.Prototype:
return ScriptElementKind.localClassElement;
default: {
assertType<never>(kind);
return ScriptElementKind.unknown;
}
}
case SyntaxKind.Identifier:
return isImportClause(node.parent) ? ScriptElementKind.alias : ScriptElementKind.unknown;
case SyntaxKind.ExportAssignment:
const scriptKind = getNodeKind((node as ExportAssignment).expression);
// If the expression didn't come back with something (like it does for an identifiers)
return scriptKind === ScriptElementKind.unknown ? ScriptElementKind.constElement : scriptKind;
default:
return ScriptElementKind.unknown;
}
function getKindOfVariableDeclaration(v: VariableDeclaration): ScriptElementKind {
return isVarConst(v)
? ScriptElementKind.constElement
: isLet(v)
? ScriptElementKind.letElement
: ScriptElementKind.variableElement;
}
}
export function isThis(node: Node): boolean {
switch (node.kind) {
case SyntaxKind.ThisKeyword:
// case SyntaxKind.ThisType: TODO: GH#9267
return true;
case SyntaxKind.Identifier:
// 'this' as a parameter
return identifierIsThisKeyword(node as Identifier) && node.parent.kind === SyntaxKind.Parameter;
default:
return false;
}
}
// Matches the beginning of a triple slash directive
const tripleSlashDirectivePrefixRegex = /^\/\/\/\s*</;
export interface ListItemInfo {
listItemIndex: number;
list: Node;
}
export function getLineStartPositionForPosition(position: number, sourceFile: SourceFileLike): number {
const lineStarts = getLineStarts(sourceFile);
const line = sourceFile.getLineAndCharacterOfPosition(position).line;
return lineStarts[line];
}
export function rangeContainsRange(r1: TextRange, r2: TextRange): boolean {
return startEndContainsRange(r1.pos, r1.end, r2);
}
export function rangeContainsRangeExclusive(r1: TextRange, r2: TextRange): boolean {
return rangeContainsPositionExclusive(r1, r2.pos) && rangeContainsPositionExclusive(r1, r2.end);
}
export function rangeContainsPosition(r: TextRange, pos: number): boolean {
return r.pos <= pos && pos <= r.end;
}
export function rangeContainsPositionExclusive(r: TextRange, pos: number) {
return r.pos < pos && pos < r.end;
}
export function startEndContainsRange(start: number, end: number, range: TextRange): boolean {
return start <= range.pos && end >= range.end;
}
export function rangeContainsStartEnd(range: TextRange, start: number, end: number): boolean {
return range.pos <= start && range.end >= end;
}
export function rangeOverlapsWithStartEnd(r1: TextRange, start: number, end: number) {
return startEndOverlapsWithStartEnd(r1.pos, r1.end, start, end);
}
export function nodeOverlapsWithStartEnd(node: Node, sourceFile: SourceFile, start: number, end: number) {
return startEndOverlapsWithStartEnd(node.getStart(sourceFile), node.end, start, end);
}
export function startEndOverlapsWithStartEnd(start1: number, end1: number, start2: number, end2: number) {
const start = Math.max(start1, start2);
const end = Math.min(end1, end2);
return start < end;
}
/**
* Assumes `candidate.start <= position` holds.
*/
export function positionBelongsToNode(candidate: Node, position: number, sourceFile: SourceFile): boolean {
Debug.assert(candidate.pos <= position);
return position < candidate.end || !isCompletedNode(candidate, sourceFile);
}
function isCompletedNode(n: Node | undefined, sourceFile: SourceFile): boolean {
if (n === undefined || nodeIsMissing(n)) {
return false;
}
switch (n.kind) {
case SyntaxKind.ClassDeclaration:
case SyntaxKind.InterfaceDeclaration:
case SyntaxKind.EnumDeclaration:
case SyntaxKind.ObjectLiteralExpression:
case SyntaxKind.ObjectBindingPattern:
case SyntaxKind.TypeLiteral:
case SyntaxKind.Block:
case SyntaxKind.ModuleBlock:
case SyntaxKind.CaseBlock:
case SyntaxKind.NamedImports:
case SyntaxKind.NamedExports:
return nodeEndsWith(n, SyntaxKind.CloseBraceToken, sourceFile);
case SyntaxKind.CatchClause:
return isCompletedNode((<CatchClause>n).block, sourceFile);
case SyntaxKind.NewExpression:
if (!(<NewExpression>n).arguments) {
return true;
}
// falls through
case SyntaxKind.CallExpression:
case SyntaxKind.ParenthesizedExpression:
case SyntaxKind.ParenthesizedType:
return nodeEndsWith(n, SyntaxKind.CloseParenToken, sourceFile);
case SyntaxKind.FunctionType:
case SyntaxKind.ConstructorType:
return isCompletedNode((<SignatureDeclaration>n).type, sourceFile);
case SyntaxKind.Constructor:
case SyntaxKind.GetAccessor:
case SyntaxKind.SetAccessor:
case SyntaxKind.FunctionDeclaration:
case SyntaxKind.FunctionExpression:
case SyntaxKind.MethodDeclaration:
case SyntaxKind.MethodSignature:
case SyntaxKind.ConstructSignature:
case SyntaxKind.CallSignature:
case SyntaxKind.ArrowFunction:
if ((<FunctionLikeDeclaration>n).body) {
return isCompletedNode((<FunctionLikeDeclaration>n).body, sourceFile);
}
if ((<FunctionLikeDeclaration>n).type) {
return isCompletedNode((<FunctionLikeDeclaration>n).type, sourceFile);
}
// Even though type parameters can be unclosed, we can get away with
// having at least a closing paren.
return hasChildOfKind(n, SyntaxKind.CloseParenToken, sourceFile);
case SyntaxKind.ModuleDeclaration:
return !!(<ModuleDeclaration>n).body && isCompletedNode((<ModuleDeclaration>n).body, sourceFile);
case SyntaxKind.IfStatement:
if ((<IfStatement>n).elseStatement) {
return isCompletedNode((<IfStatement>n).elseStatement, sourceFile);
}
return isCompletedNode((<IfStatement>n).thenStatement, sourceFile);
case SyntaxKind.ExpressionStatement:
return isCompletedNode((<ExpressionStatement>n).expression, sourceFile) ||
hasChildOfKind(n, SyntaxKind.SemicolonToken, sourceFile);
case SyntaxKind.ArrayLiteralExpression:
case SyntaxKind.ArrayBindingPattern:
case SyntaxKind.ElementAccessExpression:
case SyntaxKind.ComputedPropertyName:
case SyntaxKind.TupleType:
return nodeEndsWith(n, SyntaxKind.CloseBracketToken, sourceFile);
case SyntaxKind.IndexSignature:
if ((<IndexSignatureDeclaration>n).type) {
return isCompletedNode((<IndexSignatureDeclaration>n).type, sourceFile);
}
return hasChildOfKind(n, SyntaxKind.CloseBracketToken, sourceFile);
case SyntaxKind.CaseClause:
case SyntaxKind.DefaultClause:
// there is no such thing as terminator token for CaseClause/DefaultClause so for simplicity always consider them non-completed
return false;
case SyntaxKind.ForStatement:
case SyntaxKind.ForInStatement:
case SyntaxKind.ForOfStatement:
case SyntaxKind.WhileStatement:
return isCompletedNode((<IterationStatement>n).statement, sourceFile);
case SyntaxKind.DoStatement:
// rough approximation: if DoStatement has While keyword - then if node is completed is checking the presence of ')';
return hasChildOfKind(n, SyntaxKind.WhileKeyword, sourceFile)
? nodeEndsWith(n, SyntaxKind.CloseParenToken, sourceFile)
: isCompletedNode((<DoStatement>n).statement, sourceFile);
case SyntaxKind.TypeQuery:
return isCompletedNode((<TypeQueryNode>n).exprName, sourceFile);
case SyntaxKind.TypeOfExpression:
case SyntaxKind.DeleteExpression:
case SyntaxKind.VoidExpression:
case SyntaxKind.YieldExpression:
case SyntaxKind.SpreadElement:
const unaryWordExpression = n as (TypeOfExpression | DeleteExpression | VoidExpression | YieldExpression | SpreadElement);
return isCompletedNode(unaryWordExpression.expression, sourceFile);
case SyntaxKind.TaggedTemplateExpression:
return isCompletedNode((<TaggedTemplateExpression>n).template, sourceFile);
case SyntaxKind.TemplateExpression:
const lastSpan = lastOrUndefined((<TemplateExpression>n).templateSpans);
return isCompletedNode(lastSpan, sourceFile);
case SyntaxKind.TemplateSpan:
return nodeIsPresent((<TemplateSpan>n).literal);
case SyntaxKind.ExportDeclaration:
case SyntaxKind.ImportDeclaration:
return nodeIsPresent((<ExportDeclaration | ImportDeclaration>n).moduleSpecifier);
case SyntaxKind.PrefixUnaryExpression:
return isCompletedNode((<PrefixUnaryExpression>n).operand, sourceFile);
case SyntaxKind.BinaryExpression:
return isCompletedNode((<BinaryExpression>n).right, sourceFile);
case SyntaxKind.ConditionalExpression:
return isCompletedNode((<ConditionalExpression>n).whenFalse, sourceFile);
default:
return true;
}
}
/*
* Checks if node ends with 'expectedLastToken'.
* If child at position 'length - 1' is 'SemicolonToken' it is skipped and 'expectedLastToken' is compared with child at position 'length - 2'.
*/
function nodeEndsWith(n: Node, expectedLastToken: SyntaxKind, sourceFile: SourceFile): boolean {
const children = n.getChildren(sourceFile);
if (children.length) {
const lastChild = last(children);
if (lastChild.kind === expectedLastToken) {
return true;
}
else if (lastChild.kind === SyntaxKind.SemicolonToken && children.length !== 1) {
return children[children.length - 2].kind === expectedLastToken;
}
}
return false;
}
export function findListItemInfo(node: Node): ListItemInfo | undefined {
const list = findContainingList(node);
// It is possible at this point for syntaxList to be undefined, either if
// node.parent had no list child, or if none of its list children contained
// the span of node. If this happens, return undefined. The caller should
// handle this case.
if (!list) {
return undefined;
}
const children = list.getChildren();
const listItemIndex = indexOfNode(children, node);
return {
listItemIndex,
list
};
}
export function hasChildOfKind(n: Node, kind: SyntaxKind, sourceFile: SourceFile): boolean {
return !!findChildOfKind(n, kind, sourceFile);
}
export function findChildOfKind<T extends Node>(n: Node, kind: T["kind"], sourceFile: SourceFileLike): T | undefined {
return find(n.getChildren(sourceFile), (c): c is T => c.kind === kind);
}
export function findContainingList(node: Node): SyntaxList | undefined {
// The node might be a list element (nonsynthetic) or a comma (synthetic). Either way, it will
// be parented by the container of the SyntaxList, not the SyntaxList itself.
// In order to find the list item index, we first need to locate SyntaxList itself and then search
// for the position of the relevant node (or comma).
const syntaxList = find(node.parent.getChildren(), (c): c is SyntaxList => isSyntaxList(c) && rangeContainsRange(c, node));
// Either we didn't find an appropriate list, or the list must contain us.
Debug.assert(!syntaxList || contains(syntaxList.getChildren(), node));
return syntaxList;
}
function isDefaultModifier(node: Node) {
return node.kind === SyntaxKind.DefaultKeyword;
}
function isClassKeyword(node: Node) {
return node.kind === SyntaxKind.ClassKeyword;
}
function isFunctionKeyword(node: Node) {
return node.kind === SyntaxKind.FunctionKeyword;
}
function getAdjustedLocationForClass(node: ClassDeclaration | ClassExpression) {
if (isNamedDeclaration(node)) {
return node.name;
}
if (isClassDeclaration(node)) {
// for class and function declarations, use the `default` modifier
// when the declaration is unnamed.
const defaultModifier = find(node.modifiers!, isDefaultModifier);
if (defaultModifier) return defaultModifier;
}
if (isClassExpression(node)) {
// for class expressions, use the `class` keyword when the class is unnamed
const classKeyword = find(node.getChildren(), isClassKeyword);
if (classKeyword) return classKeyword;
}
}
function getAdjustedLocationForFunction(node: FunctionDeclaration | FunctionExpression) {
if (isNamedDeclaration(node)) {
return node.name;
}
if (isFunctionDeclaration(node)) {
// for class and function declarations, use the `default` modifier
// when the declaration is unnamed.
const defaultModifier = find(node.modifiers!, isDefaultModifier);
if (defaultModifier) return defaultModifier;
}
if (isFunctionExpression(node)) {
// for function expressions, use the `function` keyword when the function is unnamed
const functionKeyword = find(node.getChildren(), isFunctionKeyword);
if (functionKeyword) return functionKeyword;
}
}
function getAdjustedLocationForDeclaration(node: Node, forRename: boolean) {
if (!forRename) {
switch (node.kind) {
case SyntaxKind.ClassDeclaration:
case SyntaxKind.ClassExpression:
return getAdjustedLocationForClass(node as ClassDeclaration | ClassExpression);
case SyntaxKind.FunctionDeclaration:
case SyntaxKind.FunctionExpression:
return getAdjustedLocationForFunction(node as FunctionDeclaration | FunctionExpression);
}
}
if (isNamedDeclaration(node)) {
return node.name;
}
}
function getAdjustedLocationForImportDeclaration(node: ImportDeclaration, forRename: boolean) {
if (node.importClause) {
if (node.importClause.name && node.importClause.namedBindings) {
// do not adjust if we have both a name and named bindings
return;
}
// /**/import [|name|] from ...;
// import /**/type [|name|] from ...;
if (node.importClause.name) {
return node.importClause.name;
}
// /**/import { [|name|] } from ...;
// /**/import { propertyName as [|name|] } from ...;
// /**/import * as [|name|] from ...;
// import /**/type { [|name|] } from ...;
// import /**/type { propertyName as [|name|] } from ...;
// import /**/type * as [|name|] from ...;
if (node.importClause.namedBindings) {
if (isNamedImports(node.importClause.namedBindings)) {
// do nothing if there is more than one binding
const onlyBinding = singleOrUndefined(node.importClause.namedBindings.elements);
if (!onlyBinding) {
return;
}
return onlyBinding.name;
}
else if (isNamespaceImport(node.importClause.namedBindings)) {
return node.importClause.namedBindings.name;
}
}
}
if (!forRename) {
// /**/import "[|module|]";
// /**/import ... from "[|module|]";
// import /**/type ... from "[|module|]";
return node.moduleSpecifier;
}
}
function getAdjustedLocationForExportDeclaration(node: ExportDeclaration, forRename: boolean) {
if (node.exportClause) {
// /**/export { [|name|] } ...
// /**/export { propertyName as [|name|] } ...
// /**/export * as [|name|] ...
// export /**/type { [|name|] } from ...
// export /**/type { propertyName as [|name|] } from ...
// export /**/type * as [|name|] ...
if (isNamedExports(node.exportClause)) {
// do nothing if there is more than one binding
const onlyBinding = singleOrUndefined(node.exportClause.elements);
if (!onlyBinding) {
return;
}
return node.exportClause.elements[0].name;
}
else if (isNamespaceExport(node.exportClause)) {
return node.exportClause.name;
}
}
if (!forRename) {
// /**/export * from "[|module|]";
// export /**/type * from "[|module|]";
return node.moduleSpecifier;
}
}
function getAdjustedLocationForHeritageClause(node: HeritageClause) {
// /**/extends [|name|]
// /**/implements [|name|]
if (node.types.length === 1) {
return node.types[0].expression;
}
// /**/extends name1, name2 ...
// /**/implements name1, name2 ...
}
function getAdjustedLocation(node: Node, forRename: boolean): Node {
const { parent } = node;
// /**/<modifier> [|name|] ...
// /**/<modifier> <class|interface|type|enum|module|namespace|function|get|set> [|name|] ...
// /**/<class|interface|type|enum|module|namespace|function|get|set> [|name|] ...
// /**/import [|name|] = ...
//
// NOTE: If the node is a modifier, we don't adjust its location if it is the `default` modifier as that is handled
// specially by `getSymbolAtLocation`.
if (isModifier(node) && (forRename || node.kind !== SyntaxKind.DefaultKeyword) ? contains(parent.modifiers, node) :
node.kind === SyntaxKind.ClassKeyword ? isClassDeclaration(parent) || isClassExpression(node) :
node.kind === SyntaxKind.FunctionKeyword ? isFunctionDeclaration(parent) || isFunctionExpression(node) :
node.kind === SyntaxKind.InterfaceKeyword ? isInterfaceDeclaration(parent) :
node.kind === SyntaxKind.EnumKeyword ? isEnumDeclaration(parent) :
node.kind === SyntaxKind.TypeKeyword ? isTypeAliasDeclaration(parent) :
node.kind === SyntaxKind.NamespaceKeyword || node.kind === SyntaxKind.ModuleKeyword ? isModuleDeclaration(parent) :
node.kind === SyntaxKind.ImportKeyword ? isImportEqualsDeclaration(parent) :
node.kind === SyntaxKind.GetKeyword ? isGetAccessorDeclaration(parent) :
node.kind === SyntaxKind.SetKeyword && isSetAccessorDeclaration(parent)) {
const location = getAdjustedLocationForDeclaration(parent, forRename);
if (location) {
return location;
}
}
// /**/<var|let|const> [|name|] ...
if ((node.kind === SyntaxKind.VarKeyword || node.kind === SyntaxKind.ConstKeyword || node.kind === SyntaxKind.LetKeyword) &&
isVariableDeclarationList(parent) && parent.declarations.length === 1) {
const decl = parent.declarations[0];
if (isIdentifier(decl.name)) {
return decl.name;
}
}
if (node.kind === SyntaxKind.TypeKeyword) {
// import /**/type [|name|] from ...;
// import /**/type { [|name|] } from ...;
// import /**/type { propertyName as [|name|] } from ...;
// import /**/type ... from "[|module|]";
if (isImportClause(parent) && parent.isTypeOnly) {
const location = getAdjustedLocationForImportDeclaration(parent.parent, forRename);
if (location) {
return location;
}
}
// export /**/type { [|name|] } from ...;
// export /**/type { propertyName as [|name|] } from ...;
// export /**/type * from "[|module|]";
// export /**/type * as ... from "[|module|]";
if (isExportDeclaration(parent) && parent.isTypeOnly) {
const location = getAdjustedLocationForExportDeclaration(parent, forRename);
if (location) {
return location;
}
}
}
// import { propertyName /**/as [|name|] } ...
// import * /**/as [|name|] ...
// export { propertyName /**/as [|name|] } ...
// export * /**/as [|name|] ...
if (node.kind === SyntaxKind.AsKeyword) {
if (isImportSpecifier(parent) && parent.propertyName ||
isExportSpecifier(parent) && parent.propertyName ||
isNamespaceImport(parent) ||
isNamespaceExport(parent)) {
return parent.name;
}
if (isExportDeclaration(parent) && parent.exportClause && isNamespaceExport(parent.exportClause)) {
return parent.exportClause.name;
}
}
// /**/import [|name|] from ...;
// /**/import { [|name|] } from ...;
// /**/import { propertyName as [|name|] } from ...;
// /**/import ... from "[|module|]";
// /**/import "[|module|]";
if (node.kind === SyntaxKind.ImportKeyword && isImportDeclaration(parent)) {
const location = getAdjustedLocationForImportDeclaration(parent, forRename);
if (location) {
return location;
}
}
if (node.kind === SyntaxKind.ExportKeyword) {
// /**/export { [|name|] } ...;
// /**/export { propertyName as [|name|] } ...;
// /**/export * from "[|module|]";
// /**/export * as ... from "[|module|]";
if (isExportDeclaration(parent)) {
const location = getAdjustedLocationForExportDeclaration(parent, forRename);
if (location) {
return location;
}
}
// NOTE: We don't adjust the location of the `default` keyword as that is handled specially by `getSymbolAtLocation`.
// /**/export default [|name|];
// /**/export = [|name|];
if (isExportAssignment(parent)) {
return skipOuterExpressions(parent.expression);
}
}
// import name = /**/require("[|module|]");
if (node.kind === SyntaxKind.RequireKeyword && isExternalModuleReference(parent)) {
return parent.expression;
}
// import ... /**/from "[|module|]";
// export ... /**/from "[|module|]";
if (node.kind === SyntaxKind.FromKeyword && (isImportDeclaration(parent) || isExportDeclaration(parent)) && parent.moduleSpecifier) {
return parent.moduleSpecifier;
}
// class ... /**/extends [|name|] ...
// class ... /**/implements [|name|] ...
// class ... /**/implements name1, name2 ...
// interface ... /**/extends [|name|] ...
// interface ... /**/extends name1, name2 ...
if ((node.kind === SyntaxKind.ExtendsKeyword || node.kind === SyntaxKind.ImplementsKeyword) && isHeritageClause(parent) && parent.token === node.kind) {
const location = getAdjustedLocationForHeritageClause(parent);
if (location) {
return location;
}
}
if (node.kind === SyntaxKind.ExtendsKeyword) {
// ... <T /**/extends [|U|]> ...
if (isTypeParameterDeclaration(parent) && parent.constraint && isTypeReferenceNode(parent.constraint)) {
return parent.constraint.typeName;
}
// ... T /**/extends [|U|] ? ...
if (isConditionalTypeNode(parent) && isTypeReferenceNode(parent.extendsType)) {
return parent.extendsType.typeName;
}
}
// ... T extends /**/infer [|U|] ? ...
if (node.kind === SyntaxKind.InferKeyword && isInferTypeNode(parent)) {
return parent.typeParameter.name;
}
// { [ [|K|] /**/in keyof T]: ... }
if (node.kind === SyntaxKind.InKeyword && isTypeParameterDeclaration(parent) && isMappedTypeNode(parent.parent)) {
return parent.name;
}
// /**/keyof [|T|]
if (node.kind === SyntaxKind.KeyOfKeyword && isTypeOperatorNode(parent) && parent.operator === SyntaxKind.KeyOfKeyword &&
isTypeReferenceNode(parent.type)) {
return parent.type.typeName;
}
// /**/readonly [|name|][]
if (node.kind === SyntaxKind.ReadonlyKeyword && isTypeOperatorNode(parent) && parent.operator === SyntaxKind.ReadonlyKeyword &&
isArrayTypeNode(parent.type) && isTypeReferenceNode(parent.type.elementType)) {
return parent.type.elementType.typeName;
}
if (!forRename) {
// /**/new [|name|]
// /**/void [|name|]
// /**/void obj.[|name|]
// /**/typeof [|name|]
// /**/typeof obj.[|name|]
// /**/await [|name|]
// /**/await obj.[|name|]
// /**/yield [|name|]
// /**/yield obj.[|name|]
// /**/delete obj.[|name|]
if (node.kind === SyntaxKind.NewKeyword && isNewExpression(parent) ||
node.kind === SyntaxKind.VoidKeyword && isVoidExpression(parent) ||
node.kind === SyntaxKind.TypeOfKeyword && isTypeOfExpression(parent) ||
node.kind === SyntaxKind.AwaitKeyword && isAwaitExpression(parent) ||
node.kind === SyntaxKind.YieldKeyword && isYieldExpression(parent) ||
node.kind === SyntaxKind.DeleteKeyword && isDeleteExpression(parent)) {
if (parent.expression) {
return skipOuterExpressions(parent.expression);
}
}
// left /**/in [|name|]
// left /**/instanceof [|name|]
if ((node.kind === SyntaxKind.InKeyword || node.kind === SyntaxKind.InstanceOfKeyword) && isBinaryExpression(parent) && parent.operatorToken === node) {
return skipOuterExpressions(parent.right);
}
// left /**/as [|name|]
if (node.kind === SyntaxKind.AsKeyword && isAsExpression(parent) && isTypeReferenceNode(parent.type)) {
return parent.type.typeName;
}
// for (... /**/in [|name|])
// for (... /**/of [|name|])
if (node.kind === SyntaxKind.InKeyword && isForInStatement(parent) ||
node.kind === SyntaxKind.OfKeyword && isForOfStatement(parent)) {
return skipOuterExpressions(parent.expression);
}
}
return node;
}
/**
* Adjusts the location used for "find references" and "go to definition" when the cursor was not
* on a property name.
*/
export function getAdjustedReferenceLocation(node: Node): Node {
return getAdjustedLocation(node, /*forRename*/ false);
}
/**
* Adjusts the location used for "rename" when the cursor was not on a property name.
*/
export function getAdjustedRenameLocation(node: Node): Node {
return getAdjustedLocation(node, /*forRename*/ true);
}
/**
* Gets the token whose text has range [start, end) and
* position >= start and (position < end or (position === end && token is literal or keyword or identifier))
*/
export function getTouchingPropertyName(sourceFile: SourceFile, position: number): Node {
return getTouchingToken(sourceFile, position, n => isPropertyNameLiteral(n) || isKeyword(n.kind) || isPrivateIdentifier(n));
}
/**
* Returns the token if position is in [start, end).
* If position === end, returns the preceding token if includeItemAtEndPosition(previousToken) === true
*/
export function getTouchingToken(sourceFile: SourceFile, position: number, includePrecedingTokenAtEndPosition?: (n: Node) => boolean): Node {
return getTokenAtPositionWorker(sourceFile, position, /*allowPositionInLeadingTrivia*/ false, includePrecedingTokenAtEndPosition, /*includeEndPosition*/ false);
}
/** Returns a token if position is in [start-of-leading-trivia, end) */
export function getTokenAtPosition(sourceFile: SourceFile, position: number): Node {
return getTokenAtPositionWorker(sourceFile, position, /*allowPositionInLeadingTrivia*/ true, /*includePrecedingTokenAtEndPosition*/ undefined, /*includeEndPosition*/ false);
}
/** Get the token whose text contains the position */
function getTokenAtPositionWorker(sourceFile: SourceFile, position: number, allowPositionInLeadingTrivia: boolean, includePrecedingTokenAtEndPosition: ((n: Node) => boolean) | undefined, includeEndPosition: boolean): Node {
let current: Node = sourceFile;
outer: while (true) {
// find the child that contains 'position'
for (const child of current.getChildren(sourceFile)) {
const start = allowPositionInLeadingTrivia ? child.getFullStart() : child.getStart(sourceFile, /*includeJsDoc*/ true);
if (start > position) {
// If this child begins after position, then all subsequent children will as well.
break;
}
const end = child.getEnd();
if (position < end || (position === end && (child.kind === SyntaxKind.EndOfFileToken || includeEndPosition))) {
current = child;
continue outer;
}
else if (includePrecedingTokenAtEndPosition && end === position) {
const previousToken = findPrecedingToken(position, sourceFile, child);
if (previousToken && includePrecedingTokenAtEndPosition(previousToken)) {
return previousToken;
}
}
}
return current;
}
}
/**
* The token on the left of the position is the token that strictly includes the position
* or sits to the left of the cursor if it is on a boundary. For example
*
* fo|o -> will return foo
* foo <comment> |bar -> will return foo
*
*/
export function findTokenOnLeftOfPosition(file: SourceFile, position: number): Node | undefined {
// Ideally, getTokenAtPosition should return a token. However, it is currently
// broken, so we do a check to make sure the result was indeed a token.
const tokenAtPosition = getTokenAtPosition(file, position);
if (isToken(tokenAtPosition) && position > tokenAtPosition.getStart(file) && position < tokenAtPosition.getEnd()) {
return tokenAtPosition;
}
return findPrecedingToken(position, file);
}
export function findNextToken(previousToken: Node, parent: Node, sourceFile: SourceFileLike): Node | undefined {
return find(parent);
function find(n: Node): Node | undefined {
if (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(sourceFile), 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, sourceFile) ? find(child) : undefined;
});
}
}
/**
* Finds the rightmost token satisfying `token.end <= position`,
* excluding `JsxText` tokens containing only whitespace.
*/
export function findPrecedingToken(position: number, sourceFile: SourceFile, startNode?: Node, excludeJsdoc?: boolean): Node | undefined {
const result = find(startNode || sourceFile);
Debug.assert(!(result && isWhiteSpaceOnlyJsxText(result)));
return result;
function find(n: Node): Node | undefined {
if (isNonWhitespaceToken(n) && n.kind !== SyntaxKind.EndOfFileToken) {
return n;
}
const children = n.getChildren(sourceFile);
for (let i = 0; i < children.length; i++) {
const child = children[i];
// Note that the span of a node's tokens is [node.getStart(...), node.end).
// Given that `position < child.end` and child has constituent tokens, we distinguish these cases:
// 1) `position` precedes `child`'s tokens or `child` has no tokens (ie: in a comment or whitespace preceding `child`):
// we need to find the last token in a previous child.
// 2) `position` is within the same span: we recurse on `child`.
if (position < child.end) {
const start = child.getStart(sourceFile, /*includeJsDoc*/ !excludeJsdoc);
const lookInPreviousChild =
(start >= position) || // cursor in the leading trivia
!nodeHasTokens(child, sourceFile) ||
isWhiteSpaceOnlyJsxText(child);
if (lookInPreviousChild) {
// actual start of the node is past the position - previous token should be at the end of previous child
const candidate = findRightmostChildNodeWithTokens(children, /*exclusiveStartPosition*/ i, sourceFile);
return candidate && findRightmostToken(candidate, sourceFile);
}
else {
// candidate should be in this node
return find(child);
}
}
}
Debug.assert(startNode !== undefined || n.kind === SyntaxKind.SourceFile || n.kind === SyntaxKind.EndOfFileToken || isJSDocCommentContainingNode(n));
// Here we know that none of child token nodes embrace the position,
// the only known case is when position is at the end of the file.
// Try to find the rightmost token in the file without filtering.
// Namely we are skipping the check: 'position < node.end'
const candidate = findRightmostChildNodeWithTokens(children, /*exclusiveStartPosition*/ children.length, sourceFile);
return candidate && findRightmostToken(candidate, sourceFile);
}
}
function isNonWhitespaceToken(n: Node): boolean {
return isToken(n) && !isWhiteSpaceOnlyJsxText(n);
}
function findRightmostToken(n: Node, sourceFile: SourceFile): Node | undefined {
if (isNonWhitespaceToken(n)) {
return n;
}
const children = n.getChildren(sourceFile);
const candidate = findRightmostChildNodeWithTokens(children, /*exclusiveStartPosition*/ children.length, sourceFile);
return candidate && findRightmostToken(candidate, sourceFile);
}
/**
* Finds the rightmost child to the left of `children[exclusiveStartPosition]` which is a non-all-whitespace token or has constituent tokens.
*/
function findRightmostChildNodeWithTokens(children: Node[], exclusiveStartPosition: number, sourceFile: SourceFile): Node | undefined {
for (let i = exclusiveStartPosition - 1; i >= 0; i--) {
const child = children[i];
if (isWhiteSpaceOnlyJsxText(child)) {
Debug.assert(i > 0, "`JsxText` tokens should not be the first child of `JsxElement | JsxSelfClosingElement`");
}
else if (nodeHasTokens(children[i], sourceFile)) {
return children[i];
}
}
}
export function isInString(sourceFile: SourceFile, position: number, previousToken = findPrecedingToken(position, sourceFile)): boolean {
if (previousToken && isStringTextContainingNode(previousToken)) {
const start = previousToken.getStart(sourceFile);
const end = previousToken.getEnd();
// To be "in" one of these literals, the position has to be:
// 1. entirely within the token text.
// 2. at the end position of an unterminated token.
// 3. at the end of a regular expression (due to trailing flags like '/foo/g').
if (start < position && position < end) {
return true;
}
if (position === end) {
return !!(<LiteralExpression>previousToken).isUnterminated;
}
}
return false;
}
/**
* returns true if the position is in between the open and close elements of an JSX expression.
*/
export function isInsideJsxElementOrAttribute(sourceFile: SourceFile, position: number) {
const token = getTokenAtPosition(sourceFile, position);
if (!token) {
return false;
}
if (token.kind === SyntaxKind.JsxText) {
return true;
}
// <div>Hello |</div>
if (token.kind === SyntaxKind.LessThanToken && token.parent.kind === SyntaxKind.JsxText) {
return true;
}
// <div> { | </div> or <div a={| </div>
if (token.kind === SyntaxKind.LessThanToken && token.parent.kind === SyntaxKind.JsxExpression) {
return true;
}
// <div> {
// |
// } < /div>
if (token && token.kind === SyntaxKind.CloseBraceToken && token.parent.kind === SyntaxKind.JsxExpression) {
return true;
}
// <div>|</div>
if (token.kind === SyntaxKind.LessThanToken && token.parent.kind === SyntaxKind.JsxClosingElement) {
return true;
}
return false;
}
function isWhiteSpaceOnlyJsxText(node: Node): boolean {
return isJsxText(node) && node.containsOnlyTriviaWhiteSpaces;
}
export function isInTemplateString(sourceFile: SourceFile, position: number) {
const token = getTokenAtPosition(sourceFile, position);
return isTemplateLiteralKind(token.kind) && position > token.getStart(sourceFile);
}
export function isInJSXText(sourceFile: SourceFile, position: number) {
const token = getTokenAtPosition(sourceFile, position);
if (isJsxText(token)) {
return true;
}
if (token.kind === SyntaxKind.OpenBraceToken && isJsxExpression(token.parent) && isJsxElement(token.parent.parent)) {
return true;
}
if (token.kind === SyntaxKind.LessThanToken && isJsxOpeningLikeElement(token.parent) && isJsxElement(token.parent.parent)) {
return true;
}
return false;
}
export function findPrecedingMatchingToken(token: Node, matchingTokenKind: SyntaxKind, sourceFile: SourceFile) {
const tokenKind = token.kind;
let remainingMatchingTokens = 0;
while (true) {
const preceding = findPrecedingToken(token.getFullStart(), sourceFile);
if (!preceding) {
return undefined;
}
token = preceding;
if (token.kind === matchingTokenKind) {
if (remainingMatchingTokens === 0) {
return token;
}
remainingMatchingTokens--;
}
else if (token.kind === tokenKind) {
remainingMatchingTokens++;
}
}
}
export function removeOptionality(type: Type, isOptionalExpression: boolean, isOptionalChain: boolean) {
return isOptionalExpression ? type.getNonNullableType() :
isOptionalChain ? type.getNonOptionalType() :
type;
}
export function isPossiblyTypeArgumentPosition(token: Node, sourceFile: SourceFile, checker: TypeChecker): boolean {
const info = getPossibleTypeArgumentsInfo(token, sourceFile);
return info !== undefined && (isPartOfTypeNode(info.called) ||
getPossibleGenericSignatures(info.called, info.nTypeArguments, checker).length !== 0 ||
isPossiblyTypeArgumentPosition(info.called, sourceFile, checker));
}
export function getPossibleGenericSignatures(called: Expression, typeArgumentCount: number, checker: TypeChecker): readonly Signature[] {
let type = checker.getTypeAtLocation(called);
if (isOptionalChain(called.parent)) {
type = removeOptionality(type, !!called.parent.questionDotToken, /*isOptionalChain*/ true);
}
const signatures = isNewExpression(called.parent) ? type.getConstructSignatures() : type.getCallSignatures();
return signatures.filter(candidate => !!candidate.typeParameters && candidate.typeParameters.length >= typeArgumentCount);
}
export interface PossibleTypeArgumentInfo {
readonly called: Identifier;
readonly nTypeArguments: number;
}
export interface PossibleProgramFileInfo {
ProgramFiles?: string[];
}
// Get info for an expression like `f <` that may be the start of type arguments.
export function getPossibleTypeArgumentsInfo(tokenIn: Node, sourceFile: SourceFile): PossibleTypeArgumentInfo | undefined {
let token: Node | undefined = tokenIn;
// This function determines if the node could be type argument position
// Since during editing, when type argument list is not complete,
// the tree could be of any shape depending on the tokens parsed before current node,
// scanning of the previous identifier followed by "<" before current node would give us better result
// Note that we also balance out the already provided type arguments, arrays, object literals while doing so
let remainingLessThanTokens = 0;
let nTypeArguments = 0;
while (token) {
switch (token.kind) {
case SyntaxKind.LessThanToken:
// Found the beginning of the generic argument expression
token = findPrecedingToken(token.getFullStart(), sourceFile);
if (token && token.kind === SyntaxKind.QuestionDotToken) {
token = findPrecedingToken(token.getFullStart(), sourceFile);
}
if (!token || !isIdentifier(token)) return undefined;
if (!remainingLessThanTokens) {
return isDeclarationName(token) ? undefined : { called: token, nTypeArguments };
}
remainingLessThanTokens--;
break;
case SyntaxKind.GreaterThanGreaterThanGreaterThanToken:
remainingLessThanTokens = + 3;
break;
case SyntaxKind.GreaterThanGreaterThanToken:
remainingLessThanTokens = + 2;
break;
case SyntaxKind.GreaterThanToken:
remainingLessThanTokens++;
break;
case SyntaxKind.CloseBraceToken:
// This can be object type, skip until we find the matching open brace token
// Skip until the matching open brace token
token = findPrecedingMatchingToken(token, SyntaxKind.OpenBraceToken, sourceFile);
if (!token) return undefined;
break;
case SyntaxKind.CloseParenToken:
// This can be object type, skip until we find the matching open brace token
// Skip until the matching open brace token
token = findPrecedingMatchingToken(token, SyntaxKind.OpenParenToken, sourceFile);
if (!token) return undefined;
break;
case SyntaxKind.CloseBracketToken:
// This can be object type, skip until we find the matching open brace token
// Skip until the matching open brace token
token = findPrecedingMatchingToken(token, SyntaxKind.OpenBracketToken, sourceFile);
if (!token) return undefined;
break;
// Valid tokens in a type name. Skip.
case SyntaxKind.CommaToken:
nTypeArguments++;
break;
case SyntaxKind.EqualsGreaterThanToken:
// falls through
case SyntaxKind.Identifier:
case SyntaxKind.StringLiteral:
case SyntaxKind.NumericLiteral:
case SyntaxKind.BigIntLiteral:
case SyntaxKind.TrueKeyword:
case SyntaxKind.FalseKeyword:
// falls through
case SyntaxKind.TypeOfKeyword:
case SyntaxKind.ExtendsKeyword:
case SyntaxKind.KeyOfKeyword:
case SyntaxKind.DotToken:
case SyntaxKind.BarToken:
case SyntaxKind.QuestionToken:
case SyntaxKind.ColonToken:
break;
default:
if (isTypeNode(token)) {
break;
}
// Invalid token in type
return undefined;
}
token = findPrecedingToken(token.getFullStart(), sourceFile);
}
return undefined;
}
/**
* Returns true if the cursor at position in sourceFile is within a comment.
*
* @param tokenAtPosition Must equal `getTokenAtPosition(sourceFile, position)
* @param predicate Additional predicate to test on the comment range.
*/
export function isInComment(sourceFile: SourceFile, position: number, tokenAtPosition?: Node): CommentRange | undefined {
return formatting.getRangeOfEnclosingComment(sourceFile, position, /*precedingToken*/ undefined, tokenAtPosition);
}
export function hasDocComment(sourceFile: SourceFile, position: number): boolean {
const token = getTokenAtPosition(sourceFile, position);
return !!findAncestor(token, isJSDoc);
}
function nodeHasTokens(n: Node, sourceFile: SourceFileLike): boolean {
// 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 as EndOfFileToken).jsDoc : n.getWidth(sourceFile) !== 0;
}
export function getNodeModifiers(node: Node): string {
const flags = isDeclaration(node) ? getCombinedModifierFlags(node) : ModifierFlags.None;
const result: string[] = [];
if (flags & ModifierFlags.Private) result.push(ScriptElementKindModifier.privateMemberModifier);
if (flags & ModifierFlags.Protected) result.push(ScriptElementKindModifier.protectedMemberModifier);
if (flags & ModifierFlags.Public) result.push(ScriptElementKindModifier.publicMemberModifier);
if (flags & ModifierFlags.Static) result.push(ScriptElementKindModifier.staticModifier);
if (flags & ModifierFlags.Abstract) result.push(ScriptElementKindModifier.abstractModifier);
if (flags & ModifierFlags.Export) result.push(ScriptElementKindModifier.exportedModifier);
if (node.flags & NodeFlags.Ambient) result.push(ScriptElementKindModifier.ambientModifier);
if (node.kind === SyntaxKind.ExportAssignment) result.push(ScriptElementKindModifier.exportedModifier);
return result.length > 0 ? result.join(",") : ScriptElementKindModifier.none;
}
export function getTypeArgumentOrTypeParameterList(node: Node): NodeArray<Node> | undefined {
if (node.kind === SyntaxKind.TypeReference || node.kind === SyntaxKind.CallExpression) {
return (<CallExpression>node).typeArguments;
}
if (isFunctionLike(node) || node.kind === SyntaxKind.ClassDeclaration || node.kind === SyntaxKind.InterfaceDeclaration) {
return (<FunctionLikeDeclaration>node).typeParameters;
}
return undefined;
}
export function isComment(kind: SyntaxKind): boolean {
return kind === SyntaxKind.SingleLineCommentTrivia || kind === SyntaxKind.MultiLineCommentTrivia;
}
export function isStringOrRegularExpressionOrTemplateLiteral(kind: SyntaxKind): boolean {
if (kind === SyntaxKind.StringLiteral
|| kind === SyntaxKind.RegularExpressionLiteral
|| isTemplateLiteralKind(kind)) {
return true;
}
return false;
}
export function isPunctuation(kind: SyntaxKind): boolean {
return SyntaxKind.FirstPunctuation <= kind && kind <= SyntaxKind.LastPunctuation;
}
export function isInsideTemplateLiteral(node: TemplateLiteralToken, position: number, sourceFile: SourceFile): boolean {
return isTemplateLiteralKind(node.kind)
&& (node.getStart(sourceFile) < position && position < node.end) || (!!node.isUnterminated && position === node.end);
}
export function isAccessibilityModifier(kind: SyntaxKind) {
switch (kind) {
case SyntaxKind.PublicKeyword:
case SyntaxKind.PrivateKeyword:
case SyntaxKind.ProtectedKeyword:
return true;
}
return false;
}
export function cloneCompilerOptions(options: CompilerOptions): CompilerOptions {
const result = clone(options);
setConfigFileInOptions(result, options && options.configFile);
return result;
}
export function isArrayLiteralOrObjectLiteralDestructuringPattern(node: Node) {
if (node.kind === SyntaxKind.ArrayLiteralExpression ||
node.kind === SyntaxKind.ObjectLiteralExpression) {
// [a,b,c] from:
// [a, b, c] = someExpression;
if (node.parent.kind === SyntaxKind.BinaryExpression &&
(<BinaryExpression>node.parent).left === node &&
(<BinaryExpression>node.parent).operatorToken.kind === SyntaxKind.EqualsToken) {
return true;
}
// [a, b, c] from:
// for([a, b, c] of expression)
if (node.parent.kind === SyntaxKind.ForOfStatement &&
(<ForOfStatement>node.parent).initializer === node) {
return true;
}
// [a, b, c] of
// [x, [a, b, c] ] = someExpression
// or
// {x, a: {a, b, c} } = someExpression
if (isArrayLiteralOrObjectLiteralDestructuringPattern(node.parent.kind === SyntaxKind.PropertyAssignment ? node.parent.parent : node.parent)) {
return true;
}
}
return false;
}
export function isInReferenceComment(sourceFile: SourceFile, position: number): boolean {
return isInReferenceCommentWorker(sourceFile, position, /*shouldBeReference*/ true);
}
export function isInNonReferenceComment(sourceFile: SourceFile, position: number): boolean {
return isInReferenceCommentWorker(sourceFile, position, /*shouldBeReference*/ false);
}
function isInReferenceCommentWorker(sourceFile: SourceFile, position: number, shouldBeReference: boolean): boolean {
const range = isInComment(sourceFile, position, /*tokenAtPosition*/ undefined);
return !!range && shouldBeReference === tripleSlashDirectivePrefixRegex.test(sourceFile.text.substring(range.pos, range.end));
}
export function createTextSpanFromNode(node: Node, sourceFile?: SourceFile, endNode?: Node): TextSpan {
return createTextSpanFromBounds(node.getStart(sourceFile), (endNode || node).getEnd());
}
export function createTextRangeFromNode(node: Node, sourceFile: SourceFile): TextRange {
return createRange(node.getStart(sourceFile), node.end);
}
export function createTextSpanFromRange(range: TextRange): TextSpan {
return createTextSpanFromBounds(range.pos, range.end);
}
export function createTextRangeFromSpan(span: TextSpan): TextRange {
return createRange(span.start, span.start + span.length);
}
export function createTextChangeFromStartLength(start: number, length: number, newText: string): TextChange {
return createTextChange(createTextSpan(start, length), newText);
}
export function createTextChange(span: TextSpan, newText: string): TextChange {
return { span, newText };
}
export const typeKeywords: readonly SyntaxKind[] = [
SyntaxKind.AnyKeyword,
SyntaxKind.AssertsKeyword,
SyntaxKind.BigIntKeyword,
SyntaxKind.BooleanKeyword,
SyntaxKind.FalseKeyword,
SyntaxKind.KeyOfKeyword,
SyntaxKind.NeverKeyword,
SyntaxKind.NullKeyword,
SyntaxKind.NumberKeyword,
SyntaxKind.ObjectKeyword,
SyntaxKind.ReadonlyKeyword,
SyntaxKind.StringKeyword,
SyntaxKind.SymbolKeyword,
SyntaxKind.TrueKeyword,
SyntaxKind.VoidKeyword,
SyntaxKind.UndefinedKeyword,
SyntaxKind.UniqueKeyword,
SyntaxKind.UnknownKeyword,
];
export function isTypeKeyword(kind: SyntaxKind): boolean {
return contains(typeKeywords, kind);
}
export function isTypeKeywordToken(node: Node): node is Token<SyntaxKind.TypeKeyword> {
return node.kind === SyntaxKind.TypeKeyword;
}
/** True if the symbol is for an external module, as opposed to a namespace. */
export function isExternalModuleSymbol(moduleSymbol: Symbol): boolean {
return !!(moduleSymbol.flags & SymbolFlags.Module) && moduleSymbol.name.charCodeAt(0) === CharacterCodes.doubleQuote;
}
/** Returns `true` the first time it encounters a node and `false` afterwards. */
export type NodeSeenTracker<T = Node> = (node: T) => boolean;
export function nodeSeenTracker<T extends Node>(): NodeSeenTracker<T> {
const seen: true[] = [];
return node => {
const id = getNodeId(node);
return !seen[id] && (seen[id] = true);
};
}
export function getSnapshotText(snap: IScriptSnapshot): string {
return snap.getText(0, snap.getLength());
}
export function repeatString(str: string, count: number): string {
let result = "";
for (let i = 0; i < count; i++) {
result += str;
}
return result;
}
export function skipConstraint(type: Type): Type {
return type.isTypeParameter() ? type.getConstraint() || type : type;
}
export function getNameFromPropertyName(name: PropertyName): string | undefined {
return name.kind === SyntaxKind.ComputedPropertyName
// treat computed property names where expression is string/numeric literal as just string/numeric literal
? isStringOrNumericLiteralLike(name.expression) ? name.expression.text : undefined
: isPrivateIdentifier(name) ? idText(name) : getTextOfIdentifierOrLiteral(name);
}
export function programContainsEs6Modules(program: Program): boolean {
return program.getSourceFiles().some(s => !s.isDeclarationFile && !program.isSourceFileFromExternalLibrary(s) && !!s.externalModuleIndicator);
}
export function compilerOptionsIndicateEs6Modules(compilerOptions: CompilerOptions): boolean {
return !!compilerOptions.module || compilerOptions.target! >= ScriptTarget.ES2015 || !!compilerOptions.noEmit;
}
export function hostUsesCaseSensitiveFileNames(host: { useCaseSensitiveFileNames?(): boolean; }): boolean {
return host.useCaseSensitiveFileNames ? host.useCaseSensitiveFileNames() : false;
}
export function hostGetCanonicalFileName(host: { useCaseSensitiveFileNames?(): boolean; }): GetCanonicalFileName {
return createGetCanonicalFileName(hostUsesCaseSensitiveFileNames(host));
}
export function makeImportIfNecessary(defaultImport: Identifier | undefined, namedImports: readonly ImportSpecifier[] | undefined, moduleSpecifier: string, quotePreference: QuotePreference): ImportDeclaration | undefined {
return defaultImport || namedImports && namedImports.length ? makeImport(defaultImport, namedImports, moduleSpecifier, quotePreference) : undefined;
}
export function makeImport(defaultImport: Identifier | undefined, namedImports: readonly ImportSpecifier[] | undefined, moduleSpecifier: string | Expression, quotePreference: QuotePreference, isTypeOnly?: boolean): ImportDeclaration {
return createImportDeclaration(
/*decorators*/ undefined,
/*modifiers*/ undefined,
defaultImport || namedImports
? createImportClause(defaultImport, namedImports && namedImports.length ? createNamedImports(namedImports) : undefined, isTypeOnly)
: undefined,
typeof moduleSpecifier === "string" ? makeStringLiteral(moduleSpecifier, quotePreference) : moduleSpecifier);
}
export function makeStringLiteral(text: string, quotePreference: QuotePreference): StringLiteral {
return createLiteral(text, quotePreference === QuotePreference.Single);
}
export const enum QuotePreference { Single, Double }
export function quotePreferenceFromString(str: StringLiteral, sourceFile: SourceFile): QuotePreference {
return isStringDoubleQuoted(str, sourceFile) ? QuotePreference.Double : QuotePreference.Single;
}
export function getQuotePreference(sourceFile: SourceFile, preferences: UserPreferences): QuotePreference {
if (preferences.quotePreference && preferences.quotePreference !== "auto") {
return preferences.quotePreference === "single" ? QuotePreference.Single : QuotePreference.Double;
}
else {
const firstModuleSpecifier = sourceFile.imports && find(sourceFile.imports, isStringLiteral);
return firstModuleSpecifier ? quotePreferenceFromString(firstModuleSpecifier, sourceFile) : QuotePreference.Double;
}
}
export function getQuoteFromPreference(qp: QuotePreference): string {
switch (qp) {
case QuotePreference.Single: return "'";
case QuotePreference.Double: return '"';
default: return Debug.assertNever(qp);
}
}
export function symbolNameNoDefault(symbol: Symbol): string | undefined {
const escaped = symbolEscapedNameNoDefault(symbol);
return escaped === undefined ? undefined : unescapeLeadingUnderscores(escaped);
}
export function symbolEscapedNameNoDefault(symbol: Symbol): __String | undefined {
if (symbol.escapedName !== InternalSymbolName.Default) {
return symbol.escapedName;
}
return firstDefined(symbol.declarations, decl => {
const name = getNameOfDeclaration(decl);
return name && name.kind === SyntaxKind.Identifier ? name.escapedText : undefined;
});
}
export type ObjectBindingElementWithoutPropertyName = BindingElement & { name: Identifier };
export function isObjectBindingElementWithoutPropertyName(bindingElement: Node): bindingElement is ObjectBindingElementWithoutPropertyName {
return isBindingElement(bindingElement) &&
isObjectBindingPattern(bindingElement.parent) &&
isIdentifier(bindingElement.name) &&
!bindingElement.propertyName;
}
export function getPropertySymbolFromBindingElement(checker: TypeChecker, bindingElement: ObjectBindingElementWithoutPropertyName): Symbol | undefined {
const typeOfPattern = checker.getTypeAtLocation(bindingElement.parent);
return typeOfPattern && checker.getPropertyOfType(typeOfPattern, bindingElement.name.text);
}
/**
* Find symbol of the given property-name and add the symbol to the given result array
* @param symbol a symbol to start searching for the given propertyName
* @param propertyName a name of property to search for
* @param result an array of symbol of found property symbols
* @param previousIterationSymbolsCache a cache of symbol from previous iterations of calling this function to prevent infinite revisiting of the same symbol.
* The value of previousIterationSymbol is undefined when the function is first called.
*/
export function getPropertySymbolsFromBaseTypes<T>(symbol: Symbol, propertyName: string, checker: TypeChecker, cb: (symbol: Symbol) => T | undefined): T | undefined {
const seen = createMap<true>();
return recur(symbol);
function recur(symbol: Symbol): T | undefined {
// Use `addToSeen` to ensure we don't infinitely recurse in this situation:
// interface C extends C {
// /*findRef*/propName: string;
// }
if (!(symbol.flags & (SymbolFlags.Class | SymbolFlags.Interface)) || !addToSeen(seen, getSymbolId(symbol))) return;
return firstDefined(symbol.declarations, declaration => firstDefined(getAllSuperTypeNodes(declaration), typeReference => {
const type = checker.getTypeAtLocation(typeReference);
const propertySymbol = type && type.symbol && checker.getPropertyOfType(type, propertyName);
// Visit the typeReference as well to see if it directly or indirectly uses that property
return type && propertySymbol && (firstDefined(checker.getRootSymbols(propertySymbol), cb) || recur(type.symbol));
}));
}
}
export function isMemberSymbolInBaseType(memberSymbol: Symbol, checker: TypeChecker): boolean {
return getPropertySymbolsFromBaseTypes(memberSymbol.parent!, memberSymbol.name, checker, _ => true) || false;
}
export function getParentNodeInSpan(node: Node | undefined, file: SourceFile, span: TextSpan): Node | undefined {
if (!node) return undefined;
while (node.parent) {
if (isSourceFile(node.parent) || !spanContainsNode(span, node.parent, file)) {
return node;
}
node = node.parent;
}
}
function spanContainsNode(span: TextSpan, node: Node, file: SourceFile): boolean {
return textSpanContainsPosition(span, node.getStart(file)) &&
node.getEnd() <= textSpanEnd(span);
}
export function findModifier(node: Node, kind: Modifier["kind"]): Modifier | undefined {
return node.modifiers && find(node.modifiers, m => m.kind === kind);
}
export function insertImport(changes: textChanges.ChangeTracker, sourceFile: SourceFile, importDecl: Statement, blankLineBetween: boolean): void {
const lastImportDeclaration = findLast(sourceFile.statements, isAnyImportSyntax);
if (lastImportDeclaration) {
changes.insertNodeAfter(sourceFile, lastImportDeclaration, importDecl);
}
else {
changes.insertNodeAtTopOfFile(sourceFile, importDecl, blankLineBetween);
}
}
export function getTypeKeywordOfTypeOnlyImport(importClause: ImportClause, sourceFile: SourceFile): Token<SyntaxKind.TypeKeyword> {
Debug.assert(importClause.isTypeOnly);
return cast(importClause.getChildAt(0, sourceFile), isTypeKeywordToken);
}
export function textSpansEqual(a: TextSpan | undefined, b: TextSpan | undefined): boolean {
return !!a && !!b && a.start === b.start && a.length === b.length;
}
export function documentSpansEqual(a: DocumentSpan, b: DocumentSpan): boolean {
return a.fileName === b.fileName && textSpansEqual(a.textSpan, b.textSpan);
}
/**
* Iterates through 'array' by index and performs the callback on each element of array until the callback
* returns a truthy value, then returns that value.
* If no such value is found, the callback is applied to each element of array and undefined is returned.
*/
export function forEachUnique<T, U>(array: readonly T[] | undefined, callback: (element: T, index: number) => U): U | undefined {
if (array) {
for (let i = 0; i < array.length; i++) {
if (array.indexOf(array[i]) === i) {
const result = callback(array[i], i);
if (result) {
return result;
}
}
}
}
return undefined;
}
// #endregion
// Display-part writer helpers
// #region
export function isFirstDeclarationOfSymbolParameter(symbol: Symbol) {
return symbol.declarations && symbol.declarations.length > 0 && symbol.declarations[0].kind === SyntaxKind.Parameter;
}
const displayPartWriter = getDisplayPartWriter();
function getDisplayPartWriter(): DisplayPartsSymbolWriter {
const absoluteMaximumLength = defaultMaximumTruncationLength * 10; // A hard cutoff to avoid overloading the messaging channel in worst-case scenarios
let displayParts: SymbolDisplayPart[];
let lineStart: boolean;
let indent: number;
let length: number;
resetWriter();
const unknownWrite = (text: string) => writeKind(text, SymbolDisplayPartKind.text);
return {
displayParts: () => {
const finalText = displayParts.length && displayParts[displayParts.length - 1].text;
if (length > absoluteMaximumLength && finalText && finalText !== "...") {
if (!isWhiteSpaceLike(finalText.charCodeAt(finalText.length - 1))) {
displayParts.push(displayPart(" ", SymbolDisplayPartKind.space));
}
displayParts.push(displayPart("...", SymbolDisplayPartKind.punctuation));
}
return displayParts;
},
writeKeyword: text => writeKind(text, SymbolDisplayPartKind.keyword),
writeOperator: text => writeKind(text, SymbolDisplayPartKind.operator),
writePunctuation: text => writeKind(text, SymbolDisplayPartKind.punctuation),
writeTrailingSemicolon: text => writeKind(text, SymbolDisplayPartKind.punctuation),
writeSpace: text => writeKind(text, SymbolDisplayPartKind.space),
writeStringLiteral: text => writeKind(text, SymbolDisplayPartKind.stringLiteral),
writeParameter: text => writeKind(text, SymbolDisplayPartKind.parameterName),
writeProperty: text => writeKind(text, SymbolDisplayPartKind.propertyName),
writeLiteral: text => writeKind(text, SymbolDisplayPartKind.stringLiteral),
writeSymbol,
writeLine,
write: unknownWrite,
writeComment: unknownWrite,
getText: () => "",
getTextPos: () => 0,
getColumn: () => 0,
getLine: () => 0,
isAtStartOfLine: () => false,
hasTrailingWhitespace: () => false,
hasTrailingComment: () => false,
rawWrite: notImplemented,
getIndent: () => indent,
increaseIndent: () => { indent++; },
decreaseIndent: () => { indent--; },
clear: resetWriter,
trackSymbol: noop,
reportInaccessibleThisError: noop,
reportInaccessibleUniqueSymbolError: noop,
reportPrivateInBaseOfClassExpression: noop,
};
function writeIndent() {
if (length > absoluteMaximumLength) return;
if (lineStart) {
const indentString = getIndentString(indent);
if (indentString) {
length += indentString.length;
displayParts.push(displayPart(indentString, SymbolDisplayPartKind.space));
}
lineStart = false;
}
}
function writeKind(text: string, kind: SymbolDisplayPartKind) {
if (length > absoluteMaximumLength) return;
writeIndent();
length += text.length;
displayParts.push(displayPart(text, kind));
}
function writeSymbol(text: string, symbol: Symbol) {
if (length > absoluteMaximumLength) return;
writeIndent();
length += text.length;
displayParts.push(symbolPart(text, symbol));
}
function writeLine() {
if (length > absoluteMaximumLength) return;
length += 1;
displayParts.push(lineBreakPart());
lineStart = true;
}
function resetWriter() {
displayParts = [];
lineStart = true;
indent = 0;
length = 0;
}
}
export function symbolPart(text: string, symbol: Symbol) {
return displayPart(text, displayPartKind(symbol));
function displayPartKind(symbol: Symbol): SymbolDisplayPartKind {
const flags = symbol.flags;
if (flags & SymbolFlags.Variable) {
return isFirstDeclarationOfSymbolParameter(symbol) ? SymbolDisplayPartKind.parameterName : SymbolDisplayPartKind.localName;
}
else if (flags & SymbolFlags.Property) { return SymbolDisplayPartKind.propertyName; }
else if (flags & SymbolFlags.GetAccessor) { return SymbolDisplayPartKind.propertyName; }
else if (flags & SymbolFlags.SetAccessor) { return SymbolDisplayPartKind.propertyName; }
else if (flags & SymbolFlags.EnumMember) { return SymbolDisplayPartKind.enumMemberName; }
else if (flags & SymbolFlags.Function) { return SymbolDisplayPartKind.functionName; }
else if (flags & SymbolFlags.Class) { return SymbolDisplayPartKind.className; }
else if (flags & SymbolFlags.Interface) { return SymbolDisplayPartKind.interfaceName; }
else if (flags & SymbolFlags.Enum) { return SymbolDisplayPartKind.enumName; }
else if (flags & SymbolFlags.Module) { return SymbolDisplayPartKind.moduleName; }
else if (flags & SymbolFlags.Method) { return SymbolDisplayPartKind.methodName; }
else if (flags & SymbolFlags.TypeParameter) { return SymbolDisplayPartKind.typeParameterName; }
else if (flags & SymbolFlags.TypeAlias) { return SymbolDisplayPartKind.aliasName; }
else if (flags & SymbolFlags.Alias) { return SymbolDisplayPartKind.aliasName; }
return SymbolDisplayPartKind.text;
}
}
export function displayPart(text: string, kind: SymbolDisplayPartKind): SymbolDisplayPart {
return { text, kind: SymbolDisplayPartKind[kind] };
}
export function spacePart() {
return displayPart(" ", SymbolDisplayPartKind.space);
}
export function keywordPart(kind: SyntaxKind) {
return displayPart(tokenToString(kind)!, SymbolDisplayPartKind.keyword);
}
export function punctuationPart(kind: SyntaxKind) {
return displayPart(tokenToString(kind)!, SymbolDisplayPartKind.punctuation);
}
export function operatorPart(kind: SyntaxKind) {
return displayPart(tokenToString(kind)!, SymbolDisplayPartKind.operator);
}
export function textOrKeywordPart(text: string) {
const kind = stringToToken(text);
return kind === undefined
? textPart(text)
: keywordPart(kind);
}
export function textPart(text: string) {
return displayPart(text, SymbolDisplayPartKind.text);
}
const carriageReturnLineFeed = "\r\n";
/**
* The default is CRLF.
*/
export function getNewLineOrDefaultFromHost(host: LanguageServiceHost | LanguageServiceShimHost, formatSettings?: FormatCodeSettings) {
return (formatSettings && formatSettings.newLineCharacter) ||
(host.getNewLine && host.getNewLine()) ||
carriageReturnLineFeed;
}
export function lineBreakPart() {
return displayPart("\n", SymbolDisplayPartKind.lineBreak);
}
export function mapToDisplayParts(writeDisplayParts: (writer: DisplayPartsSymbolWriter) => void): SymbolDisplayPart[] {
try {
writeDisplayParts(displayPartWriter);
return displayPartWriter.displayParts();
}
finally {
displayPartWriter.clear();
}
}
export function typeToDisplayParts(typechecker: TypeChecker, type: Type, enclosingDeclaration?: Node, flags: TypeFormatFlags = TypeFormatFlags.None): SymbolDisplayPart[] {
return mapToDisplayParts(writer => {
typechecker.writeType(type, enclosingDeclaration, flags | TypeFormatFlags.MultilineObjectLiterals | TypeFormatFlags.UseAliasDefinedOutsideCurrentScope, writer);
});
}
export function symbolToDisplayParts(typeChecker: TypeChecker, symbol: Symbol, enclosingDeclaration?: Node, meaning?: SymbolFlags, flags: SymbolFormatFlags = SymbolFormatFlags.None): SymbolDisplayPart[] {
return mapToDisplayParts(writer => {
typeChecker.writeSymbol(symbol, enclosingDeclaration, meaning, flags | SymbolFormatFlags.UseAliasDefinedOutsideCurrentScope, writer);
});
}
export function signatureToDisplayParts(typechecker: TypeChecker, signature: Signature, enclosingDeclaration?: Node, flags: TypeFormatFlags = TypeFormatFlags.None): SymbolDisplayPart[] {
flags |= TypeFormatFlags.UseAliasDefinedOutsideCurrentScope | TypeFormatFlags.MultilineObjectLiterals | TypeFormatFlags.WriteTypeArgumentsOfSignature | TypeFormatFlags.OmitParameterModifiers;
return mapToDisplayParts(writer => {
typechecker.writeSignature(signature, enclosingDeclaration, flags, /*signatureKind*/ undefined, writer);
});
}
export function isImportOrExportSpecifierName(location: Node): location is Identifier {
return !!location.parent && isImportOrExportSpecifier(location.parent) && location.parent.propertyName === location;
}
export function scriptKindIs(fileName: string, host: LanguageServiceHost, ...scriptKinds: ScriptKind[]): boolean {
const scriptKind = getScriptKind(fileName, host);
return some(scriptKinds, k => k === scriptKind);
}
export function getScriptKind(fileName: string, host?: LanguageServiceHost): ScriptKind {
// First check to see if the script kind was specified by the host. Chances are the host
// may override the default script kind for the file extension.
return ensureScriptKind(fileName, host && host.getScriptKind && host.getScriptKind(fileName));
}
export function getSymbolTarget(symbol: Symbol, checker: TypeChecker): Symbol {
let next: Symbol = symbol;
while (isAliasSymbol(next) || (isTransientSymbol(next) && next.target)) {
if (isTransientSymbol(next) && next.target) {
next = next.target;
}
else {
next = skipAlias(next, checker);
}
}
return next;
}
function isTransientSymbol(symbol: Symbol): symbol is TransientSymbol {
return (symbol.flags & SymbolFlags.Transient) !== 0;
}
function isAliasSymbol(symbol: Symbol): boolean {
return (symbol.flags & SymbolFlags.Alias) !== 0;
}
export function getUniqueSymbolId(symbol: Symbol, checker: TypeChecker) {
return getSymbolId(skipAlias(symbol, checker));
}
export function getFirstNonSpaceCharacterPosition(text: string, position: number) {
while (isWhiteSpaceLike(text.charCodeAt(position))) {
position += 1;
}
return position;
}
export function getPrecedingNonSpaceCharacterPosition(text: string, position: number) {
while (position > -1 && isWhiteSpaceSingleLine(text.charCodeAt(position))) {
position -= 1;
}
return position + 1;
}
/**
* Creates a deep, memberwise clone of a node with no source map location.
*
* WARNING: This is an expensive operation and is only intended to be used in refactorings
* and code fixes (because those are triggered by explicit user actions).
*/
export function getSynthesizedDeepClone<T extends Node | undefined>(node: T, includeTrivia = true): T {
const clone = node && getSynthesizedDeepCloneWorker(node as NonNullable<T>);
if (clone && !includeTrivia) suppressLeadingAndTrailingTrivia(clone);
return clone;
}
export function getSynthesizedDeepCloneWithRenames<T extends Node>(node: T, includeTrivia = true, renameMap?: Map<Identifier>, checker?: TypeChecker, callback?: (originalNode: Node, clone: Node) => any): T {
let clone;
if (renameMap && checker && isBindingElement(node) && isIdentifier(node.name) && isObjectBindingPattern(node.parent)) {
const symbol = checker.getSymbolAtLocation(node.name);
const renameInfo = symbol && renameMap.get(String(getSymbolId(symbol)));
if (renameInfo && renameInfo.text !== (node.name || node.propertyName).getText()) {
clone = createBindingElement(
node.dotDotDotToken,
node.propertyName || node.name,
renameInfo,
node.initializer);
}
}
else if (renameMap && checker && isIdentifier(node)) {
const symbol = checker.getSymbolAtLocation(node);
const renameInfo = symbol && renameMap.get(String(getSymbolId(symbol)));
if (renameInfo) {
clone = createIdentifier(renameInfo.text);
}
}
if (!clone) {
clone = getSynthesizedDeepCloneWorker(node as NonNullable<T>, renameMap, checker, callback);
}
if (clone && !includeTrivia) suppressLeadingAndTrailingTrivia(clone);
if (callback && clone) callback(node, clone);
return clone as T;
}
function getSynthesizedDeepCloneWorker<T extends Node>(node: T, renameMap?: Map<Identifier>, checker?: TypeChecker, callback?: (originalNode: Node, clone: Node) => any): T {
const visited = (renameMap || checker || callback) ?
visitEachChild(node, wrapper, nullTransformationContext) :
visitEachChild(node, getSynthesizedDeepClone, nullTransformationContext);
if (visited === node) {
// This only happens for leaf nodes - internal nodes always see their children change.
const clone = getSynthesizedClone(node);
if (isStringLiteral(clone)) {
clone.textSourceNode = node as any;
}
else if (isNumericLiteral(clone)) {
clone.numericLiteralFlags = (node as any).numericLiteralFlags;
}
return setTextRange(clone, node);
}
// PERF: As an optimization, rather than calling getSynthesizedClone, we'll update
// the new node created by visitEachChild with the extra changes getSynthesizedClone
// would have made.
visited.parent = undefined!;
return visited;
function wrapper(node: T) {
return getSynthesizedDeepCloneWithRenames(node, /*includeTrivia*/ true, renameMap, checker, callback);
}
}
export function getSynthesizedDeepClones<T extends Node>(nodes: NodeArray<T>, includeTrivia?: boolean): NodeArray<T>;
export function getSynthesizedDeepClones<T extends Node>(nodes: NodeArray<T> | undefined, includeTrivia?: boolean): NodeArray<T> | undefined;
export function getSynthesizedDeepClones<T extends Node>(nodes: NodeArray<T> | undefined, includeTrivia = true): NodeArray<T> | undefined {
return nodes && createNodeArray(nodes.map(n => getSynthesizedDeepClone(n, includeTrivia)), nodes.hasTrailingComma);
}
/**
* Sets EmitFlags to suppress leading and trailing trivia on the node.
*/
export function suppressLeadingAndTrailingTrivia(node: Node) {
suppressLeadingTrivia(node);
suppressTrailingTrivia(node);
}
/**
* Sets EmitFlags to suppress leading trivia on the node.
*/
export function suppressLeadingTrivia(node: Node) {
addEmitFlagsRecursively(node, EmitFlags.NoLeadingComments, getFirstChild);
}
/**
* Sets EmitFlags to suppress trailing trivia on the node.
*/
export function suppressTrailingTrivia(node: Node) {
addEmitFlagsRecursively(node, EmitFlags.NoTrailingComments, getLastChild);
}
function addEmitFlagsRecursively(node: Node, flag: EmitFlags, getChild: (n: Node) => Node | undefined) {
addEmitFlags(node, flag);
const child = getChild(node);
if (child) addEmitFlagsRecursively(child, flag, getChild);
}
function getFirstChild(node: Node): Node | undefined {
return node.forEachChild(child => child);
}
export function getUniqueName(baseName: string, sourceFile: SourceFile): string {
let nameText = baseName;
for (let i = 1; !isFileLevelUniqueName(sourceFile, nameText); i++) {
nameText = `${baseName}_${i}`;
}
return nameText;
}
/**
* @return The index of the (only) reference to the extracted symbol. We want the cursor
* to be on the reference, rather than the declaration, because it's closer to where the
* user was before extracting it.
*/
export function getRenameLocation(edits: readonly FileTextChanges[], renameFilename: string, name: string, preferLastLocation: boolean): number {
let delta = 0;
let lastPos = -1;
for (const { fileName, textChanges } of edits) {
Debug.assert(fileName === renameFilename);
for (const change of textChanges) {
const { span, newText } = change;
const index = indexInTextChange(newText, name);
if (index !== -1) {
lastPos = span.start + delta + index;
// If the reference comes first, return immediately.
if (!preferLastLocation) {
return lastPos;
}
}
delta += newText.length - span.length;
}
}
// If the declaration comes first, return the position of the last occurrence.
Debug.assert(preferLastLocation);
Debug.assert(lastPos >= 0);
return lastPos;
}
export function copyLeadingComments(sourceNode: Node, targetNode: Node, sourceFile: SourceFile, commentKind?: CommentKind, hasTrailingNewLine?: boolean) {
forEachLeadingCommentRange(sourceFile.text, sourceNode.pos, getAddCommentsFunction(targetNode, sourceFile, commentKind, hasTrailingNewLine, addSyntheticLeadingComment));
}
export function copyTrailingComments(sourceNode: Node, targetNode: Node, sourceFile: SourceFile, commentKind?: CommentKind, hasTrailingNewLine?: boolean) {
forEachTrailingCommentRange(sourceFile.text, sourceNode.end, getAddCommentsFunction(targetNode, sourceFile, commentKind, hasTrailingNewLine, addSyntheticTrailingComment));
}
/**
* This function copies the trailing comments for the token that comes before `sourceNode`, as leading comments of `targetNode`.
* This is useful because sometimes a comment that refers to `sourceNode` will be a leading comment for `sourceNode`, according to the
* notion of trivia ownership, and instead will be a trailing comment for the token before `sourceNode`, e.g.:
* `function foo(\* not leading comment for a *\ a: string) {}`
* The comment refers to `a` but belongs to the `(` token, but we might want to copy it.
*/
export function copyTrailingAsLeadingComments(sourceNode: Node, targetNode: Node, sourceFile: SourceFile, commentKind?: CommentKind, hasTrailingNewLine?: boolean) {
forEachTrailingCommentRange(sourceFile.text, sourceNode.pos, getAddCommentsFunction(targetNode, sourceFile, commentKind, hasTrailingNewLine, addSyntheticLeadingComment));
}
function getAddCommentsFunction(targetNode: Node, sourceFile: SourceFile, commentKind: CommentKind | undefined, hasTrailingNewLine: boolean | undefined, cb: (node: Node, kind: CommentKind, text: string, hasTrailingNewLine?: boolean) => void) {
return (pos: number, end: number, kind: CommentKind, htnl: boolean) => {
if (kind === SyntaxKind.MultiLineCommentTrivia) {
// Remove leading /*
pos += 2;
// Remove trailing */
end -= 2;
}
else {
// Remove leading //
pos += 2;
}
cb(targetNode, commentKind || kind, sourceFile.text.slice(pos, end), hasTrailingNewLine !== undefined ? hasTrailingNewLine : htnl);
};
}
function indexInTextChange(change: string, name: string): number {
if (startsWith(change, name)) return 0;
// Add a " " to avoid references inside words
let idx = change.indexOf(" " + name);
if (idx === -1) idx = change.indexOf("." + name);
if (idx === -1) idx = change.indexOf('"' + name);
return idx === -1 ? -1 : idx + 1;
}
export function getContextualTypeFromParent(node: Expression, checker: TypeChecker): Type | undefined {
const { parent } = node;
switch (parent.kind) {
case SyntaxKind.NewExpression:
return checker.getContextualType(parent as NewExpression);
case SyntaxKind.BinaryExpression: {
const { left, operatorToken, right } = parent as BinaryExpression;
return isEqualityOperatorKind(operatorToken.kind)
? checker.getTypeAtLocation(node === right ? left : right)
: checker.getContextualType(node);
}
case SyntaxKind.CaseClause:
return (parent as CaseClause).expression === node ? getSwitchedType(parent as CaseClause, checker) : undefined;
default:
return checker.getContextualType(node);
}
}
export function quote(text: string, preferences: UserPreferences): string {
// Editors can pass in undefined or empty string - we want to infer the preference in those cases.
const quotePreference = preferences.quotePreference || "auto";
const quoted = JSON.stringify(text);
switch (quotePreference) {
// TODO use getQuotePreference to infer the actual quote style.
case "auto":
case "double":
return quoted;
case "single":
return `'${stripQuotes(quoted).replace("'", "\\'").replace('\\"', '"')}'`;
default:
return Debug.assertNever(quotePreference);
}
}
export function isEqualityOperatorKind(kind: SyntaxKind): kind is EqualityOperator {
switch (kind) {
case SyntaxKind.EqualsEqualsEqualsToken:
case SyntaxKind.EqualsEqualsToken:
case SyntaxKind.ExclamationEqualsEqualsToken:
case SyntaxKind.ExclamationEqualsToken:
return true;
default:
return false;
}
}
export function isStringLiteralOrTemplate(node: Node): node is StringLiteralLike | TemplateExpression | TaggedTemplateExpression {
switch (node.kind) {
case SyntaxKind.StringLiteral:
case SyntaxKind.NoSubstitutionTemplateLiteral:
case SyntaxKind.TemplateExpression:
case SyntaxKind.TaggedTemplateExpression:
return true;
default:
return false;
}
}
export function hasIndexSignature(type: Type): boolean {
return !!type.getStringIndexType() || !!type.getNumberIndexType();
}
export function getSwitchedType(caseClause: CaseClause, checker: TypeChecker): Type | undefined {
return checker.getTypeAtLocation(caseClause.parent.parent.expression);
}
export function getTypeNodeIfAccessible(type: Type, enclosingScope: Node, program: Program, host: LanguageServiceHost): TypeNode | undefined {
const checker = program.getTypeChecker();
let typeIsAccessible = true;
const notAccessible = () => { typeIsAccessible = false; };
const res = checker.typeToTypeNode(type, enclosingScope, /*flags*/ undefined, {
trackSymbol: (symbol, declaration, meaning) => {
typeIsAccessible = typeIsAccessible && checker.isSymbolAccessible(symbol, declaration, meaning, /*shouldComputeAliasToMarkVisible*/ false).accessibility === SymbolAccessibility.Accessible;
},
reportInaccessibleThisError: notAccessible,
reportPrivateInBaseOfClassExpression: notAccessible,
reportInaccessibleUniqueSymbolError: notAccessible,
moduleResolverHost: {
readFile: host.readFile,
fileExists: host.fileExists,
directoryExists: host.directoryExists,
getSourceFiles: program.getSourceFiles,
getCurrentDirectory: program.getCurrentDirectory,
getCommonSourceDirectory: program.getCommonSourceDirectory,
}
});
return typeIsAccessible ? res : undefined;
}
export function syntaxRequiresTrailingCommaOrSemicolonOrASI(kind: SyntaxKind) {
return kind === SyntaxKind.CallSignature
|| kind === SyntaxKind.ConstructSignature
|| kind === SyntaxKind.IndexSignature
|| kind === SyntaxKind.PropertySignature
|| kind === SyntaxKind.MethodSignature;
}
export function syntaxRequiresTrailingFunctionBlockOrSemicolonOrASI(kind: SyntaxKind) {
return kind === SyntaxKind.FunctionDeclaration
|| kind === SyntaxKind.Constructor
|| kind === SyntaxKind.MethodDeclaration
|| kind === SyntaxKind.GetAccessor
|| kind === SyntaxKind.SetAccessor;
}
export function syntaxRequiresTrailingModuleBlockOrSemicolonOrASI(kind: SyntaxKind) {
return kind === SyntaxKind.ModuleDeclaration;
}
export function syntaxRequiresTrailingSemicolonOrASI(kind: SyntaxKind) {
return kind === SyntaxKind.VariableStatement
|| kind === SyntaxKind.ExpressionStatement
|| kind === SyntaxKind.DoStatement
|| kind === SyntaxKind.ContinueStatement
|| kind === SyntaxKind.BreakStatement
|| kind === SyntaxKind.ReturnStatement
|| kind === SyntaxKind.ThrowStatement
|| kind === SyntaxKind.DebuggerStatement
|| kind === SyntaxKind.PropertyDeclaration
|| kind === SyntaxKind.TypeAliasDeclaration
|| kind === SyntaxKind.ImportDeclaration
|| kind === SyntaxKind.ImportEqualsDeclaration
|| kind === SyntaxKind.ExportDeclaration
|| kind === SyntaxKind.NamespaceExportDeclaration
|| kind === SyntaxKind.ExportAssignment;
}
export const syntaxMayBeASICandidate = or(
syntaxRequiresTrailingCommaOrSemicolonOrASI,
syntaxRequiresTrailingFunctionBlockOrSemicolonOrASI,
syntaxRequiresTrailingModuleBlockOrSemicolonOrASI,
syntaxRequiresTrailingSemicolonOrASI);
function nodeIsASICandidate(node: Node, sourceFile: SourceFileLike): boolean {
const lastToken = node.getLastToken(sourceFile);
if (lastToken && lastToken.kind === SyntaxKind.SemicolonToken) {
return false;
}
if (syntaxRequiresTrailingCommaOrSemicolonOrASI(node.kind)) {
if (lastToken && lastToken.kind === SyntaxKind.CommaToken) {
return false;
}
}
else if (syntaxRequiresTrailingModuleBlockOrSemicolonOrASI(node.kind)) {
const lastChild = last(node.getChildren(sourceFile));
if (lastChild && isModuleBlock(lastChild)) {
return false;
}
}
else if (syntaxRequiresTrailingFunctionBlockOrSemicolonOrASI(node.kind)) {
const lastChild = last(node.getChildren(sourceFile));
if (lastChild && isFunctionBlock(lastChild)) {
return false;
}
}
else if (!syntaxRequiresTrailingSemicolonOrASI(node.kind)) {
return false;
}
// See comment in parsers `parseDoStatement`
if (node.kind === SyntaxKind.DoStatement) {
return true;
}
const topNode = findAncestor(node, ancestor => !ancestor.parent)!;
const nextToken = findNextToken(node, topNode, sourceFile);
if (!nextToken || nextToken.kind === SyntaxKind.CloseBraceToken) {
return true;
}
const startLine = sourceFile.getLineAndCharacterOfPosition(node.getEnd()).line;
const endLine = sourceFile.getLineAndCharacterOfPosition(nextToken.getStart(sourceFile)).line;
return startLine !== endLine;
}
export function positionIsASICandidate(pos: number, context: Node, sourceFile: SourceFileLike): boolean {
const contextAncestor = findAncestor(context, ancestor => {
if (ancestor.end !== pos) {
return "quit";
}
return syntaxMayBeASICandidate(ancestor.kind);
});
return !!contextAncestor && nodeIsASICandidate(contextAncestor, sourceFile);
}
export function probablyUsesSemicolons(sourceFile: SourceFile): boolean {
let withSemicolon = 0;
let withoutSemicolon = 0;
const nStatementsToObserve = 5;
forEachChild(sourceFile, function visit(node): boolean | undefined {
if (syntaxRequiresTrailingSemicolonOrASI(node.kind)) {
const lastToken = node.getLastToken(sourceFile);
if (lastToken && lastToken.kind === SyntaxKind.SemicolonToken) {
withSemicolon++;
}
else {
withoutSemicolon++;
}
}
if (withSemicolon + withoutSemicolon >= nStatementsToObserve) {
return true;
}
return forEachChild(node, visit);
});
// One statement missing a semicolon isnt sufficient evidence to say the user
// doesnt want semicolons, because they may not even be done writing that statement.
if (withSemicolon === 0 && withoutSemicolon <= 1) {
return true;
}
// If even 2/5 places have a semicolon, the user probably wants semicolons
return withSemicolon / withoutSemicolon > 1 / nStatementsToObserve;
}
export function tryGetDirectories(host: Pick<LanguageServiceHost, "getDirectories">, directoryName: string): string[] {
return tryIOAndConsumeErrors(host, host.getDirectories, directoryName) || [];
}
export function tryReadDirectory(host: Pick<LanguageServiceHost, "readDirectory">, path: string, extensions?: readonly string[], exclude?: readonly string[], include?: readonly string[]): readonly string[] {
return tryIOAndConsumeErrors(host, host.readDirectory, path, extensions, exclude, include) || emptyArray;
}
export function tryFileExists(host: Pick<LanguageServiceHost, "fileExists">, path: string): boolean {
return tryIOAndConsumeErrors(host, host.fileExists, path);
}
export function tryDirectoryExists(host: LanguageServiceHost, path: string): boolean {
return tryAndIgnoreErrors(() => directoryProbablyExists(path, host)) || false;
}
export function tryAndIgnoreErrors<T>(cb: () => T): T | undefined {
try { return cb(); }
catch { return undefined; }
}
export function tryIOAndConsumeErrors<T>(host: unknown, toApply: ((...a: any[]) => T) | undefined, ...args: any[]) {
return tryAndIgnoreErrors(() => toApply && toApply.apply(host, args));
}
export function findPackageJsons(startDirectory: string, host: Pick<LanguageServiceHost, "fileExists">, stopDirectory?: string): string[] {
const paths: string[] = [];
forEachAncestorDirectory(startDirectory, ancestor => {
if (ancestor === stopDirectory) {
return true;
}
const currentConfigPath = combinePaths(ancestor, "package.json");
if (tryFileExists(host, currentConfigPath)) {
paths.push(currentConfigPath);
}
});
return paths;
}
export function findPackageJson(directory: string, host: LanguageServiceHost): string | undefined {
let packageJson: string | undefined;
forEachAncestorDirectory(directory, ancestor => {
if (ancestor === "node_modules") return true;
packageJson = findConfigFile(ancestor, (f) => tryFileExists(host, f), "package.json");
if (packageJson) {
return true; // break out
}
});
return packageJson;
}
export function getPackageJsonsVisibleToFile(fileName: string, host: LanguageServiceHost): readonly PackageJsonInfo[] {
if (!host.fileExists) {
return [];
}
const packageJsons: PackageJsonInfo[] = [];
forEachAncestorDirectory(getDirectoryPath(fileName), ancestor => {
const packageJsonFileName = combinePaths(ancestor, "package.json");
if (host.fileExists!(packageJsonFileName)) {
const info = createPackageJsonInfo(packageJsonFileName, host);
if (info) {
packageJsons.push(info);
}
}
});
return packageJsons;
}
export function createPackageJsonInfo(fileName: string, host: LanguageServiceHost): PackageJsonInfo | false | undefined {
if (!host.readFile) {
return undefined;
}
type PackageJsonRaw = Record<typeof dependencyKeys[number], Record<string, string> | undefined>;
const dependencyKeys = ["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"] as const;
const stringContent = host.readFile(fileName);
if (!stringContent) return undefined;
const content = tryParseJson(stringContent) as PackageJsonRaw;
if (!content) return false;
const info: Pick<PackageJsonInfo, typeof dependencyKeys[number]> = {};
for (const key of dependencyKeys) {
const dependencies = content[key];
if (!dependencies) {
continue;
}
const dependencyMap = createMap<string>();
for (const packageName in dependencies) {
dependencyMap.set(packageName, dependencies[packageName]);
}
info[key] = dependencyMap;
}
const dependencyGroups = [
[PackageJsonDependencyGroup.Dependencies, info.dependencies],
[PackageJsonDependencyGroup.DevDependencies, info.devDependencies],
[PackageJsonDependencyGroup.OptionalDependencies, info.optionalDependencies],
[PackageJsonDependencyGroup.PeerDependencies, info.peerDependencies],
] as const;
return {
...info,
fileName,
get,
has(dependencyName, inGroups) {
return !!get(dependencyName, inGroups);
},
};
function get(dependencyName: string, inGroups = PackageJsonDependencyGroup.All) {
for (const [group, deps] of dependencyGroups) {
if (deps && (inGroups & group)) {
const dep = deps.get(dependencyName);
if (dep !== undefined) {
return dep;
}
}
}
}
}
function tryParseJson(text: string) {
try {
return JSON.parse(text);
}
catch {
return undefined;
}
}
export function consumesNodeCoreModules(sourceFile: SourceFile): boolean {
return some(sourceFile.imports, ({ text }) => JsTyping.nodeCoreModules.has(text));
}
export function isInsideNodeModules(fileOrDirectory: string): boolean {
return contains(getPathComponents(fileOrDirectory), "node_modules");
}
export function isDiagnosticWithLocation(diagnostic: Diagnostic): diagnostic is DiagnosticWithLocation {
return diagnostic.file !== undefined && diagnostic.start !== undefined && diagnostic.length !== undefined;
}
export function findDiagnosticForNode(node: Node, sortedFileDiagnostics: readonly Diagnostic[]): DiagnosticWithLocation | undefined {
const span: Partial<TextSpan> = createTextSpanFromNode(node);
const index = binarySearchKey(sortedFileDiagnostics, span, identity, compareTextSpans);
if (index >= 0) {
const diagnostic = sortedFileDiagnostics[index];
Debug.assertEqual(diagnostic.file, node.getSourceFile(), "Diagnostics proided to 'findDiagnosticForNode' must be from a single SourceFile");
return cast(diagnostic, isDiagnosticWithLocation);
}
}
export function getDiagnosticsWithinSpan(span: TextSpan, sortedFileDiagnostics: readonly Diagnostic[]): readonly DiagnosticWithLocation[] {
let index = binarySearchKey(sortedFileDiagnostics, span.start, diag => diag.start, compareValues);
if (index < 0) {
index = ~index;
}
while (sortedFileDiagnostics[index - 1]?.start === span.start) {
index--;
}
const result: DiagnosticWithLocation[] = [];
const end = textSpanEnd(span);
while (true) {
const diagnostic = tryCast(sortedFileDiagnostics[index], isDiagnosticWithLocation);
if (!diagnostic || diagnostic.start > end) {
break;
}
if (textSpanContainsTextSpan(span, diagnostic)) {
result.push(diagnostic);
}
index++;
}
return result;
}
/* @internal */
export function getRefactorContextSpan({ startPosition, endPosition }: RefactorContext): TextSpan {
return createTextSpanFromBounds(startPosition, endPosition === undefined ? startPosition : endPosition);
}
/**
* If the provided value is an array, the mapping function is applied to each element; otherwise, the mapping function is applied
* to the provided value itself.
*/
export function mapOneOrMany<T, U>(valueOrArray: T | readonly T[], f: (x: T, i: number) => U): U | U[];
export function mapOneOrMany<T, U>(valueOrArray: T | readonly T[] | undefined, f: (x: T, i: number) => U): U | U[] | undefined;
export function mapOneOrMany<T, U>(valueOrArray: T | readonly T[], f: (x: T, i: number) => U, resultSelector: (x: U[]) => U): U;
export function mapOneOrMany<T, U>(valueOrArray: T | readonly T[] | undefined, f: (x: T, i: number) => U, resultSelector: (x: U[]) => U): U | undefined;
export function mapOneOrMany<T, U>(valueOrArray: T | readonly T[] | undefined, f: (x: T, i: number) => U, resultSelector: (x: U[]) => U | U[] = identity): U | U[] | undefined {
return valueOrArray ? isArray(valueOrArray) ? resultSelector(map(valueOrArray, f)) : f(valueOrArray, 0) : undefined;
}
/**
* If the provided value is an array, the first element of the array is returned; otherwise, the provided value is returned instead.
*/
export function firstOrOnly<T>(valueOrArray: T | readonly T[]): T {
return isArray(valueOrArray) ? first(valueOrArray) : valueOrArray;
}
export function getNameForExportedSymbol(symbol: Symbol, scriptTarget: ScriptTarget) {
if (symbol.escapedName === InternalSymbolName.ExportEquals || symbol.escapedName === InternalSymbolName.Default) {
// Name of "export default foo;" is "foo". Name of "export default 0" is the filename converted to camelCase.
return firstDefined(symbol.declarations, d => isExportAssignment(d) && isIdentifier(d.expression) ? d.expression.text : undefined)
|| codefix.moduleSymbolToValidIdentifier(Debug.assertDefined(symbol.parent), scriptTarget);
}
return symbol.name;
}
// #endregion
}